Merge RoundSquisheen: bridge auto-starts L4 scheduler
This commit is contained in:
commit
7eaa3742c4
5 files changed
+1446
-153
No files matched your search
+832
-59
File diff suppressed because it is too large.
Load diff
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<title>GenericAgent</title>
|
||||
<link rel="stylesheet" href="assets/fonts/fonts.css">
|
||||
<link rel="stylesheet" href="styles.css?v=97">
|
||||
<link rel="stylesheet" href="styles.css?v=117">
|
||||
<!-- KaTeX: LaTeX 公式渲染 -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css" crossorigin="anonymous">
|
||||
<!-- highlight.js: 代码高亮 -->
|
||||
@@ -124,25 +124,40 @@
|
||||
<span data-i18n="chat.interrupting"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="composer">
|
||||
<div class="composer-top">
|
||||
<button class="ic-btn" id="upload-btn" data-i18n-title="upload.button" title="">
|
||||
<span data-ga-icon="paperclip"></span>
|
||||
</button>
|
||||
<button class="ic-btn" id="preset-btn">
|
||||
<span data-ga-icon="lightning"></span>
|
||||
</button>
|
||||
<div class="composer-anchor" id="chat-composer-anchor">
|
||||
<div class="composer" id="chat-composer">
|
||||
<div class="composer-slot">
|
||||
<div class="plan-card" id="plan-bar" hidden aria-live="polite"></div>
|
||||
<div class="composer-inset">
|
||||
<div class="thumb-strip" id="thumb-strip" hidden></div>
|
||||
<div class="input" id="chat-input" contenteditable="true" role="textbox" aria-multiline="true" data-i18n-ph="composer.placeholder"></div>
|
||||
<div class="composer-bot">
|
||||
<div class="composer-plus-wrap">
|
||||
<button type="button" class="ic-btn composer-plus" id="chat-plus-btn" aria-haspopup="menu" aria-expanded="false" data-i18n-title="collab.plusMenu" title="">
|
||||
<span data-ga-icon="plus"></span>
|
||||
</button>
|
||||
<div class="ga-menu composer-menu" id="chat-menu" hidden role="menu">
|
||||
<button type="button" class="ga-menu-item" id="chat-menu-upload" role="menuitem">
|
||||
<span data-ga-icon="paperclip"></span>
|
||||
<span data-i18n="upload.button"></span>
|
||||
</button>
|
||||
<button type="button" class="ga-menu-item" id="chat-menu-preset" role="menuitem">
|
||||
<span data-ga-icon="lightning"></span>
|
||||
<span data-i18n="modal.preset"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<span class="spacer"></span>
|
||||
<button type="button" class="chip" id="model-chip" data-i18n-title="model.menuLabel" title=""><span class="model-name"></span> <span class="chip-ic" data-ga-icon="caretDown"></span></button>
|
||||
<button type="button" class="send" id="send-btn" aria-label="send">
|
||||
<span data-ga-icon="paperPlaneTilt"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input" contenteditable="true" role="textbox" aria-multiline="true" data-i18n-ph="composer.placeholder"></div>
|
||||
<input type="file" id="img-input" multiple hidden>
|
||||
<div class="thumb-strip" id="thumb-strip" hidden></div>
|
||||
<div class="composer-bot">
|
||||
<button class="chip" id="model-chip" data-i18n-title="model.menuLabel" title=""><span class="model-name"></span> <span class="chip-ic" data-ga-icon="caretDown"></span></button>
|
||||
<button class="send" id="send-btn" type="button">
|
||||
<span data-ga-icon="paperPlaneTilt"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ga-menu" id="model-menu" hidden></div>
|
||||
<input type="file" id="chat-file-input" multiple hidden>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -233,6 +248,10 @@
|
||||
<span class="collab-rail-dot" aria-hidden="true"></span>
|
||||
<span class="n" id="collab-rail-done-n">0</span>
|
||||
</span>
|
||||
<span class="collab-rail-badge collab-rail-issue" id="collab-rail-issue" hidden>
|
||||
<span class="collab-rail-dot collab-rail-dot--danger" aria-hidden="true"></span>
|
||||
<span class="n" id="collab-rail-issue-n">0</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<aside class="collab-prog-panel" id="collab-prog-panel">
|
||||
@@ -254,12 +273,12 @@
|
||||
<button type="button" class="pill" data-prompt-key="collab.chipPause" data-i18n="collab.chipPause"></button>
|
||||
<button type="button" class="pill" data-prompt-key="collab.chipSummary" data-i18n="collab.chipSummary"></button>
|
||||
</div>
|
||||
<div class="cdb-inset">
|
||||
<div class="composer-inset cdb-inset">
|
||||
<div class="thumb-strip" id="cdb-thumb-strip" hidden></div>
|
||||
<div class="input" id="cdb-input" contenteditable="true" role="textbox" aria-multiline="true" data-i18n-ph="collab.placeholder"></div>
|
||||
<div class="composer-bot">
|
||||
<div class="cdb-plus-wrap">
|
||||
<button type="button" class="ic-btn cdb-plus" id="cdb-plus-btn" aria-haspopup="menu" aria-expanded="false" data-i18n-title="collab.plusMenu" title="">
|
||||
<div class="composer-plus-wrap cdb-plus-wrap">
|
||||
<button type="button" class="ic-btn composer-plus cdb-plus" id="cdb-plus-btn" aria-haspopup="menu" aria-expanded="false" data-i18n-title="collab.plusMenu" title="">
|
||||
<span data-ga-icon="plus"></span>
|
||||
</button>
|
||||
<div class="ga-menu cdb-menu" id="cdb-menu" hidden role="menu">
|
||||
@@ -569,6 +588,6 @@
|
||||
<!-- highlight.js -->
|
||||
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js" crossorigin="anonymous"></script>
|
||||
<script src="phosphor-icons.js"></script>
|
||||
<script src="app.js?v=88"></script>
|
||||
<script src="app.js?v=159"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -89,9 +89,9 @@
|
||||
--sample-light-bg:#f4f5f7;
|
||||
--sample-light-panel:#ffffff;
|
||||
--sample-light-line:#e9eaee;
|
||||
--sample-dark-bg:#0d1117;
|
||||
--sample-dark-panel:#161b22;
|
||||
--sample-dark-line:#30363d;
|
||||
--sample-dark-bg:#161b22;
|
||||
--sample-dark-panel:#21262d;
|
||||
--sample-dark-line:#484f58;
|
||||
|
||||
/* —— 阴影 / 遮罩 —— */
|
||||
--overlay:#14192861;
|
||||
@@ -137,17 +137,17 @@ html[data-appearance="light"]:not([data-plain="1"])[data-theme]{
|
||||
html[data-appearance="dark"]{
|
||||
--swatch-7:#ffd60a;
|
||||
--swatch-8:#e6edf3;
|
||||
--bg-base:#0d1117;
|
||||
--bg:#0d1117;
|
||||
--panel:#161b22;
|
||||
--card:#161b22;
|
||||
--field-bg:#0d1117;
|
||||
--line-base:#30363d;
|
||||
--line:#30363d;
|
||||
--line-soft-base:#21262d;
|
||||
--line-soft:#21262d;
|
||||
--line-strong:#484f58;
|
||||
--hover-2:#21262d;
|
||||
--bg-base:#161b22;
|
||||
--bg:#161b22;
|
||||
--panel:#21262d;
|
||||
--card:#21262d;
|
||||
--field-bg:#161b22;
|
||||
--line-base:#484f58;
|
||||
--line:#484f58;
|
||||
--line-soft-base:#30363d;
|
||||
--line-soft:#30363d;
|
||||
--line-strong:#656d76;
|
||||
--hover-2:#30363d;
|
||||
--hover-dim:#ffffff0f;
|
||||
--txt:#e6edf3;
|
||||
--txt-strong:#ffffff;
|
||||
@@ -155,11 +155,11 @@ html[data-appearance="dark"]{
|
||||
--txt-3:#afb8c1;
|
||||
--muted:#7d8590;
|
||||
--muted2:#656d76;
|
||||
--placeholder:#484f58;
|
||||
--placeholder:#656d76;
|
||||
--icon:#7d8590;
|
||||
--off-bg:#21262d;
|
||||
--switch-off:#484f58;
|
||||
--dot-idle:#484f58;
|
||||
--off-bg:#30363d;
|
||||
--switch-off:#656d76;
|
||||
--dot-idle:#656d76;
|
||||
--ok:#3fb950;
|
||||
--ok-strong:#46c35a;
|
||||
--ok-bg:#12261e;
|
||||
@@ -186,7 +186,8 @@ html[data-appearance="dark"][data-theme]{
|
||||
--accent-soft:color-mix(in srgb, var(--accent) 12%, var(--panel));
|
||||
}
|
||||
html[data-appearance="dark"][data-theme="7"],
|
||||
html[data-appearance="dark"][data-theme="8"]{ --on-accent:#0d1117; }
|
||||
html[data-appearance="dark"][data-theme="7"],
|
||||
html[data-appearance="dark"][data-theme="8"]{ --on-accent:#161b22; }
|
||||
|
||||
html,body{ height:100%; }
|
||||
html{ --chat-font:14px; }
|
||||
@@ -551,7 +552,7 @@ html[data-appearance="dark"] .swatch[data-theme="8"].sel::after{
|
||||
padding:5px 12px; font-size:12px; color:var(--txt-2); cursor:pointer;
|
||||
}
|
||||
.pill:hover{ border-color:var(--line-strong); }
|
||||
/* 纯状态展示组件(不可点):就绪=绿,运行中=红 */
|
||||
/* 纯状态展示组件(不可点):就绪/运行中=绿,未连接=红,连接中=灰 */
|
||||
.run-state{
|
||||
display:flex; align-items:center; gap:9px; height:30px; color:var(--ok);
|
||||
font-weight:600; font-size:12px; border:none; background:transparent;
|
||||
@@ -560,8 +561,60 @@ html[data-appearance="dark"] .swatch[data-theme="8"].sel::after{
|
||||
.run-state .dot{
|
||||
width:9px; height:9px; border-radius:50%; background:var(--ok); flex:0 0 auto;
|
||||
}
|
||||
.run-state.busy{ color:var(--danger); }
|
||||
.run-state.busy .dot{ background:var(--danger-dot); }
|
||||
.run-state.connecting{ color:var(--muted); }
|
||||
.run-state.connecting .dot{ background:var(--muted2); }
|
||||
.run-state.offline{ color:var(--danger); }
|
||||
.run-state.offline .dot{ background:var(--danger-dot); }
|
||||
.run-state.busy{ color:var(--ok); }
|
||||
|
||||
/* 呼吸灯:内圈常亮(容器 60%) + 中圈扩散(无转圈) */
|
||||
.ga-status-breathe,
|
||||
.run-state.busy .dot{
|
||||
--breathe-color:var(--ok);
|
||||
--breathe-box:12px;
|
||||
--breathe-core:calc(var(--breathe-box) * 0.6);
|
||||
position:relative;
|
||||
display:inline-block;
|
||||
width:var(--breathe-box);
|
||||
height:var(--breathe-box);
|
||||
flex:0 0 auto;
|
||||
background:transparent !important;
|
||||
border:none;
|
||||
vertical-align:middle;
|
||||
}
|
||||
.ga-status-breathe--sm{ --breathe-box:10px; }
|
||||
.run-state.busy .dot{
|
||||
width:var(--breathe-box);
|
||||
height:var(--breathe-box);
|
||||
}
|
||||
.ga-status-breathe__core,
|
||||
.run-state.busy .dot::before{
|
||||
content:'';
|
||||
position:absolute;
|
||||
left:50%; top:50%;
|
||||
width:var(--breathe-core); height:var(--breathe-core);
|
||||
transform:translate(-50%,-50%);
|
||||
border-radius:50%;
|
||||
background:var(--breathe-color);
|
||||
z-index:2;
|
||||
}
|
||||
.ga-status-breathe__ring,
|
||||
.run-state.busy .dot::after{
|
||||
content:'';
|
||||
position:absolute;
|
||||
left:50%; top:50%;
|
||||
width:var(--breathe-core); height:var(--breathe-core);
|
||||
transform:translate(-50%,-50%);
|
||||
border-radius:50%;
|
||||
border:1px solid color-mix(in srgb, var(--breathe-color) 52%, transparent);
|
||||
z-index:1;
|
||||
pointer-events:none;
|
||||
animation:ga-status-breathe 1.75s ease-out infinite;
|
||||
}
|
||||
@keyframes ga-status-breathe{
|
||||
0%{ transform:translate(-50%,-50%) scale(1); opacity:.55; }
|
||||
100%{ transform:translate(-50%,-50%) scale(2.35); opacity:0; }
|
||||
}
|
||||
.link{ color:var(--accent); cursor:pointer; }
|
||||
.muted{ color:var(--muted); }
|
||||
.ic-btn{
|
||||
@@ -677,8 +730,12 @@ html[data-appearance="dark"] .swatch[data-theme="8"].sel::after{
|
||||
.bubble .fold summary:hover{ color:var(--txt-2); }
|
||||
.bubble .fold pre{ margin:0; padding:10px 12px; border:none; background:transparent; font-size:.86em; overflow:auto; }
|
||||
.cursor{ display:inline-block; width:7px; height:14px; background:var(--accent); vertical-align:-2px; margin-left:2px; animation:blink 1s steps(2) infinite; }
|
||||
.draft-tw-plain{
|
||||
white-space:pre-wrap; word-break:break-word;
|
||||
font-family:inherit; font-size:inherit; line-height:inherit;
|
||||
}
|
||||
.draft-tw-plain:empty{ display:none; }
|
||||
@keyframes blink{ 50%{ opacity:0; } }
|
||||
.run-state.busy .dot{ animation:blink 1s steps(2) infinite; }
|
||||
|
||||
/* 预设功能卡片(现在嵌在聊天空状态)*/
|
||||
.chat-start{ width:100%; max-width:560px; }
|
||||
@@ -928,7 +985,6 @@ span.flatpickr-weekday{ color:var(--muted) !important; font-size:11px; }
|
||||
border-top:none;
|
||||
background:var(--panel);
|
||||
}
|
||||
.composer-top{ display:flex; gap:8px; margin-bottom:8px; }
|
||||
.chip{
|
||||
border:1px solid var(--line); background:transparent; border-radius:0;
|
||||
padding:0 11px; min-height:30px; font-size:12px; font-weight:700; color:var(--txt-2); cursor:pointer;
|
||||
@@ -961,18 +1017,36 @@ span.flatpickr-weekday{ color:var(--muted) !important; font-size:11px; }
|
||||
.cb-left .chip:first-child{ border-left:0; border-top-left-radius:4px; border-bottom-left-radius:4px; }
|
||||
.cb-left .chip:last-child{ border-right:0; border-top-right-radius:4px; border-bottom-right-radius:4px; }
|
||||
.cb-left .chip:only-child{ border-radius:4px; border-right:0; }
|
||||
.composer .input{
|
||||
width:100%; min-height:58px; max-height:200px; overflow-y:auto;
|
||||
.composer-inset{
|
||||
width:100%;
|
||||
border:1px solid var(--line); border-radius:var(--radius);
|
||||
padding:12px 14px; font-size:var(--chat-font); line-height:1.5; outline:none;
|
||||
font-family:inherit; background:var(--card); color:var(--txt);
|
||||
background:var(--card);
|
||||
}
|
||||
.composer-inset:focus-within{ border-color:var(--accent); }
|
||||
.composer-inset .thumb-strip{ padding:8px 12px 0; }
|
||||
.composer-inset .input{
|
||||
width:100%; min-height:58px; max-height:200px; overflow-y:auto;
|
||||
border:none; border-radius:0; background:transparent;
|
||||
padding:12px 14px 8px; font-size:var(--chat-font); line-height:1.5; outline:none;
|
||||
font-family:inherit; color:var(--txt);
|
||||
white-space:pre-wrap; word-break:break-word;
|
||||
}
|
||||
.input:focus{ border-color:var(--accent); }
|
||||
/* contenteditable 空提示(textarea 的 placeholder 等价物)*/
|
||||
.composer .input:empty::before{
|
||||
.composer-inset .input:empty::before{
|
||||
content:attr(data-ph); color:var(--muted); pointer-events:none;
|
||||
}
|
||||
.composer-inset .composer-bot{
|
||||
margin-top:0; padding:0 10px 10px; justify-content:flex-start;
|
||||
}
|
||||
.composer-inset .composer-bot .spacer{ flex:1; }
|
||||
.composer-plus-wrap{ position:relative; flex:0 0 auto; }
|
||||
.composer-plus-wrap .composer-menu,
|
||||
.composer-plus-wrap .cdb-menu{
|
||||
left:0; right:auto; top:auto; bottom:calc(100% + 6px); min-width:180px;
|
||||
}
|
||||
.composer-plus{ width:34px; height:34px; }
|
||||
.composer-inset .composer-bot > #model-chip,
|
||||
.composer-inset .composer-bot > #cdb-model-chip{ border-radius:4px; }
|
||||
/* 原子附件 chip(像 @人,光标进不去、退格整块删)*/
|
||||
.ph-chip{
|
||||
display:inline-flex; align-items:center; vertical-align:baseline;
|
||||
@@ -980,9 +1054,7 @@ span.flatpickr-weekday{ color:var(--muted) !important; font-size:11px; }
|
||||
background:color-mix(in srgb, var(--accent) 22%, transparent); color:var(--accent);
|
||||
font-size:.9em; white-space:nowrap; user-select:none; cursor:default;
|
||||
}
|
||||
.composer-bot{ display:flex; align-items:center; justify-content:flex-end; gap:8px; margin-top:8px; }
|
||||
/* 标准模型 chip 紧贴发送键时,给个小圆角避免和方正发送键看起来割裂 */
|
||||
.composer-bot > #model-chip{ border-radius:4px; }
|
||||
.composer-bot{ display:flex; align-items:center; justify-content:flex-end; gap:8px; }
|
||||
.cb-left{
|
||||
display:inline-flex; align-items:stretch; gap:0;
|
||||
border:1px solid var(--line); border-radius:4px; overflow:hidden;
|
||||
@@ -1126,6 +1198,228 @@ span.flatpickr-weekday{ color:var(--muted) !important; font-size:11px; }
|
||||
.ctx-item.danger{ color:var(--danger); }
|
||||
.ctx-item.danger:hover{ background:var(--danger-bg); }
|
||||
|
||||
/* Plan 卡片:吸附在输入框左上方(半悬浮窄卡) */
|
||||
.composer-anchor{
|
||||
position:relative;
|
||||
flex:0 0 auto;
|
||||
padding:0 16px 16px;
|
||||
background:var(--panel);
|
||||
}
|
||||
.composer-anchor > .composer{
|
||||
padding:0;
|
||||
background:transparent;
|
||||
}
|
||||
.composer-slot{
|
||||
position:relative;
|
||||
width:100%;
|
||||
}
|
||||
.plan-card{
|
||||
position:absolute;
|
||||
left:10px;
|
||||
bottom:100%;
|
||||
margin-bottom:0;
|
||||
z-index:13;
|
||||
width:min(600px, calc(68% - 10px));
|
||||
min-width:280px;
|
||||
max-width:calc(68% - 10px);
|
||||
box-sizing:border-box;
|
||||
font-size:12px;
|
||||
line-height:1.45;
|
||||
color:var(--txt);
|
||||
pointer-events:auto;
|
||||
}
|
||||
.plan-card[hidden]{ display:none !important; }
|
||||
.plan-card--expanded{
|
||||
padding:10px 12px 8px;
|
||||
background:var(--card);
|
||||
border:1px solid var(--line);
|
||||
border-bottom:none;
|
||||
border-left:3px solid var(--accent);
|
||||
border-radius:var(--radius-sm) var(--radius-sm) 0 0;
|
||||
box-shadow:0 -4px 16px #14192812, 0 -1px 0 var(--line) inset;
|
||||
}
|
||||
.plan-card--collapsed{
|
||||
width:auto;
|
||||
min-width:0;
|
||||
max-width:min(520px, 70%);
|
||||
margin-bottom:0;
|
||||
}
|
||||
/* 吸附:卡片底与输入框顶共边,视觉上连成一体 */
|
||||
.composer-slot:has(.plan-card:not([hidden])) .composer-inset{
|
||||
border-top-left-radius:0;
|
||||
border-top-right-radius:0;
|
||||
}
|
||||
.composer-slot:has(.plan-card--collapsed:not([hidden])) .plan-capsule{
|
||||
border-radius:var(--radius-sm) var(--radius-sm) 0 0;
|
||||
border-bottom:1px solid var(--line);
|
||||
box-shadow:0 -4px 12px #14192810;
|
||||
}
|
||||
.plan-card-head{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:8px;
|
||||
margin-bottom:6px;
|
||||
}
|
||||
.plan-status-dot{
|
||||
flex:0 0 auto;
|
||||
width:7px; height:7px;
|
||||
border-radius:50%;
|
||||
background:var(--accent);
|
||||
}
|
||||
.plan-card--complete .plan-status-dot{ background:var(--success); }
|
||||
.plan-card--placeholder .plan-status-dot{
|
||||
animation:plan-dot-pulse 1.4s ease-in-out infinite;
|
||||
}
|
||||
@keyframes plan-dot-pulse{
|
||||
0%,100%{ opacity:1; transform:scale(1); }
|
||||
50%{ opacity:.55; transform:scale(.92); }
|
||||
}
|
||||
.plan-title{
|
||||
flex:1 1 auto;
|
||||
font-weight:700;
|
||||
font-size:12px;
|
||||
color:var(--txt-strong);
|
||||
min-width:0;
|
||||
white-space:nowrap;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
}
|
||||
.plan-progress{
|
||||
flex:0 0 auto;
|
||||
font-weight:700;
|
||||
font-size:12px;
|
||||
color:var(--accent);
|
||||
font-family:var(--font-num);
|
||||
font-variant-numeric:var(--font-num-variant);
|
||||
}
|
||||
.plan-card--complete .plan-progress{ color:var(--success); }
|
||||
.plan-head-actions{
|
||||
flex:0 0 auto;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:4px;
|
||||
}
|
||||
.plan-btn{
|
||||
border:0;
|
||||
background:transparent;
|
||||
padding:2px 6px;
|
||||
font-size:11px;
|
||||
font-weight:600;
|
||||
color:var(--accent);
|
||||
cursor:pointer;
|
||||
border-radius:var(--radius-sm);
|
||||
font-family:var(--font-body-en);
|
||||
}
|
||||
.plan-btn:hover{ background:var(--accent-soft); }
|
||||
.plan-current{
|
||||
display:flex;
|
||||
gap:4px;
|
||||
align-items:flex-start;
|
||||
margin-bottom:8px;
|
||||
padding-left:15px;
|
||||
}
|
||||
.plan-current-label{
|
||||
flex:0 0 auto;
|
||||
color:var(--muted);
|
||||
font-size:11px;
|
||||
}
|
||||
.plan-current-text{
|
||||
flex:1 1 auto;
|
||||
color:var(--accent);
|
||||
font-weight:600;
|
||||
word-break:break-word;
|
||||
}
|
||||
.plan-card--complete .plan-current-text{ color:var(--success); }
|
||||
.plan-wait{
|
||||
color:var(--muted2);
|
||||
padding:0 0 4px 15px;
|
||||
font-size:11px;
|
||||
}
|
||||
.plan-items{
|
||||
list-style:none;
|
||||
margin:0;
|
||||
padding:0 0 4px 15px;
|
||||
}
|
||||
.plan-item{
|
||||
display:flex;
|
||||
gap:6px;
|
||||
align-items:flex-start;
|
||||
margin-top:3px;
|
||||
}
|
||||
.plan-item-mark{
|
||||
flex:0 0 auto;
|
||||
width:1.1em;
|
||||
text-align:center;
|
||||
line-height:1.45;
|
||||
}
|
||||
.plan-item-text{
|
||||
flex:1 1 auto;
|
||||
word-break:break-word;
|
||||
}
|
||||
.plan-item--done .plan-item-mark,
|
||||
.plan-item--done .plan-item-text{
|
||||
color:var(--muted);
|
||||
opacity:.75;
|
||||
}
|
||||
.plan-item--done .plan-item-text{ text-decoration:line-through; }
|
||||
.plan-item--current .plan-item-mark{ color:var(--accent); }
|
||||
.plan-item--current .plan-item-text{ color:var(--accent); font-weight:600; }
|
||||
.plan-item--pending .plan-item-mark,
|
||||
.plan-item--pending .plan-item-text{ color:var(--muted); }
|
||||
.plan-item--error .plan-item-mark,
|
||||
.plan-item--error .plan-item-text{ color:var(--danger); }
|
||||
.plan-item--warn .plan-item-mark,
|
||||
.plan-item--warn .plan-item-text{ color:#c67600; }
|
||||
html[data-appearance="dark"] .plan-item--warn .plan-item-mark,
|
||||
html[data-appearance="dark"] .plan-item--warn .plan-item-text{ color:#f0a020; }
|
||||
.plan-foot{
|
||||
display:flex;
|
||||
justify-content:flex-end;
|
||||
gap:8px;
|
||||
padding-top:4px;
|
||||
border-top:1px solid var(--line-soft);
|
||||
margin-top:4px;
|
||||
}
|
||||
.plan-more-hint{
|
||||
flex:1 1 auto;
|
||||
align-self:center;
|
||||
font-size:11px;
|
||||
color:var(--muted2);
|
||||
text-align:left;
|
||||
}
|
||||
.plan-capsule{
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
gap:8px;
|
||||
max-width:100%;
|
||||
padding:6px 12px;
|
||||
border:1px solid var(--line);
|
||||
border-left:3px solid var(--accent);
|
||||
border-radius:999px;
|
||||
background:var(--card);
|
||||
cursor:pointer;
|
||||
font-size:12px;
|
||||
font-weight:600;
|
||||
color:var(--txt);
|
||||
box-shadow:var(--shadow);
|
||||
font-family:var(--font-body-en);
|
||||
}
|
||||
.plan-capsule:hover{ border-color:var(--line-strong); background:var(--field-bg); }
|
||||
.plan-capsule-text{
|
||||
min-width:0;
|
||||
white-space:nowrap;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
color:var(--txt-2);
|
||||
}
|
||||
.plan-capsule-text em{
|
||||
font-style:normal;
|
||||
color:var(--accent);
|
||||
font-weight:700;
|
||||
}
|
||||
.plan-card--complete .plan-capsule{ border-left-color:var(--success); }
|
||||
.plan-card--complete .plan-capsule-text em{ color:var(--success); }
|
||||
|
||||
/* ─── chat composer: Plan/Auto toggle ─── */
|
||||
.chip.on{
|
||||
background:var(--accent);
|
||||
@@ -1168,9 +1462,11 @@ span.flatpickr-weekday{ color:var(--muted) !important; font-size:11px; }
|
||||
}
|
||||
.ga-menu-item:hover{ background:var(--hover-2); }
|
||||
.ga-menu-item.active{ color:var(--accent); }
|
||||
.composer-menu .ga-menu-item,
|
||||
.cdb-menu .ga-menu-item{
|
||||
display:flex; align-items:center; gap:8px;
|
||||
}
|
||||
.composer-menu .ga-menu-item svg,
|
||||
.cdb-menu .ga-menu-item svg{ width:16px; height:16px; flex:0 0 auto; color:var(--icon); }
|
||||
|
||||
/* ─── chat composer: image thumb strip ─── */
|
||||
@@ -1498,6 +1794,7 @@ span.flatpickr-weekday{ color:var(--muted) !important; font-size:11px; }
|
||||
.fold-pre{ white-space:pre-wrap; word-break:break-word; max-height:300px; overflow:auto; }
|
||||
.fold.fold-thinking{ border-color:var(--fold-thinking-border); background:var(--fold-thinking-bg); }
|
||||
.fold.fold-tool{ border:none; background:transparent; }
|
||||
.fold.fold-tool-live > summary{ opacity:.85; font-style:italic; }
|
||||
.fold.fold-result{ border:none; background:transparent; }
|
||||
.fold.fold-turn{ border-color:var(--fold-turn-border); background:var(--fold-turn-bg); }
|
||||
.fold.fold-turn > summary{ font-weight:600; color:var(--txt-2); }
|
||||
@@ -1704,7 +2001,7 @@ html[data-appearance="dark"] .bubble.md pre code{ color:var(--txt-2); }
|
||||
}
|
||||
.collab-offline[hidden], .collab-reconnect[hidden], #collab-welcome[hidden], .collab-msgs .msgs[hidden],
|
||||
.collab-rail[hidden], .collab-prog-stats[hidden],
|
||||
.collab-rail-run[hidden], .collab-rail-done[hidden]{ display:none !important; }
|
||||
.collab-rail-run[hidden], .collab-rail-done[hidden], .collab-rail-issue[hidden]{ display:none !important; }
|
||||
.collab-offline:not([hidden]), .collab-reconnect:not([hidden]){ display:flex; }
|
||||
.collab-reconnect{ background:color-mix(in srgb, var(--accent) 10%, var(--panel)); }
|
||||
.page[data-page="collab"] .msg-area.collab-msgs{ position:relative; overflow:hidden; padding:0; }
|
||||
@@ -1761,13 +2058,24 @@ html[data-appearance="dark"] .bubble.md pre code{ color:var(--txt-2); }
|
||||
35%{ transform:scale(1.12); }
|
||||
100%{ transform:scale(1); }
|
||||
}
|
||||
/* 把手运行数:转圈与呼吸上下分开,转圈内不叠绿点 */
|
||||
.collab-rail-spin{
|
||||
width:10px; height:10px; border:2px solid color-mix(in srgb, var(--ok) 25%, transparent);
|
||||
border-top-color:var(--ok); border-radius:50%; animation:collab-spin .8s linear infinite;
|
||||
display:block;
|
||||
width:12px; height:12px;
|
||||
margin:0 auto;
|
||||
border-radius:50%;
|
||||
background:transparent;
|
||||
border:2px solid color-mix(in srgb, var(--ok) 22%, transparent);
|
||||
border-top-color:var(--ok);
|
||||
animation:collab-rail-spin .8s linear infinite;
|
||||
}
|
||||
@keyframes collab-rail-spin{ to{ transform:rotate(360deg); } }
|
||||
.collab-rail-dot{ width:6px; height:6px; border-radius:50%; background:var(--ok); flex:0 0 auto; }
|
||||
.collab-rail-run .n{ color:var(--ok); }
|
||||
.collab-rail-done .n{ color:var(--ok); }
|
||||
.collab-rail-done .n{ color:var(--muted); }
|
||||
.collab-rail-done .collab-rail-dot{ background:var(--muted2); }
|
||||
.collab-rail-issue .n{ color:var(--danger); }
|
||||
.collab-rail-dot--danger{ background:var(--danger-dot); }
|
||||
.collab-prog-panel{
|
||||
position:absolute; top:0; left:0; bottom:0; width:232px; z-index:12;
|
||||
display:flex; flex-direction:column; overflow:hidden;
|
||||
@@ -1795,7 +2103,8 @@ html[data-appearance="dark"] .bubble.md pre code{ color:var(--txt-2); }
|
||||
font-size:11px; font-weight:600; padding:4px 8px; border-radius:var(--radius-sm);
|
||||
}
|
||||
.collab-stat--running{ color:var(--ok); background:color-mix(in srgb, var(--ok) 10%, var(--panel)); }
|
||||
.collab-stat--done{ color:var(--ok); background:color-mix(in srgb, var(--ok) 8%, var(--panel)); }
|
||||
.collab-stat--done{ color:var(--muted); background:color-mix(in srgb, var(--muted) 10%, var(--panel)); }
|
||||
.collab-stat--done .collab-rail-dot{ background:var(--muted2); }
|
||||
.collab-stat .n{ font-family:var(--font-num); font-variant-numeric:var(--font-num-variant); font-weight:700; }
|
||||
.collab-progress-empty{
|
||||
flex:0 0 auto; font-size:12px; color:var(--muted); line-height:1.5;
|
||||
@@ -1820,18 +2129,9 @@ html[data-appearance="dark"] .bubble.md pre code{ color:var(--txt-2); }
|
||||
.collab-card--terminated{ --collab-st:var(--line-strong); }
|
||||
.collab-card-st{ display:flex; align-items:center; gap:6px; font-size:11px; color:var(--muted); margin-bottom:6px; flex-wrap:wrap; }
|
||||
.collab-dot{ width:7px; height:7px; border-radius:50%; background:var(--collab-st); flex:0 0 auto; }
|
||||
.collab-card--running .collab-dot{ animation:collab-pulse 1.6s ease-in-out infinite; }
|
||||
@keyframes collab-pulse{
|
||||
0%,100%{ opacity:1; transform:scale(1); box-shadow:0 0 0 0 color-mix(in srgb, var(--ok) 40%, transparent); }
|
||||
50%{ opacity:.85; transform:scale(1.15); box-shadow:0 0 0 4px color-mix(in srgb, var(--ok) 0%, transparent); }
|
||||
}
|
||||
.collab-st-ic{ display:inline-flex; align-items:center; justify-content:center; width:14px; height:14px; font-size:10px; font-weight:700; flex:0 0 auto; }
|
||||
.collab-st-ic--spin{
|
||||
width:12px; height:12px; border:2px solid color-mix(in srgb, var(--ok) 25%, transparent);
|
||||
border-top-color:var(--ok); border-radius:50%; animation:collab-spin .8s linear infinite;
|
||||
}
|
||||
@keyframes collab-spin{ to{ transform:rotate(360deg); } }
|
||||
.collab-st-ic--ok{ color:var(--ok); }
|
||||
.collab-st-ic--done{ color:var(--muted); }
|
||||
.collab-st-ic--warn{ color:var(--danger); }
|
||||
.collab-st-ic--pause,.collab-st-ic--off{ color:var(--muted); }
|
||||
.collab-card-time{ margin-left:auto; font-size:10px; color:var(--muted); opacity:.85; }
|
||||
@@ -1907,27 +2207,6 @@ html[data-appearance="dark"] .bubble.md pre code{ color:var(--txt-2); }
|
||||
.page[data-page="collab"] .collab-quick{
|
||||
display:flex; flex-wrap:wrap; gap:8px; margin-bottom:10px;
|
||||
}
|
||||
.page[data-page="collab"] .cdb-inset{
|
||||
width:100%;
|
||||
border:1px solid var(--line); border-radius:var(--radius);
|
||||
background:var(--card);
|
||||
}
|
||||
.page[data-page="collab"] .cdb-inset:focus-within{ border-color:var(--accent); }
|
||||
.page[data-page="collab"] .cdb-inset .thumb-strip{ padding:8px 12px 0; }
|
||||
.page[data-page="collab"] .cdb-inset .input{
|
||||
border:none; border-radius:0; background:transparent;
|
||||
padding-bottom:8px;
|
||||
}
|
||||
.page[data-page="collab"] .cdb-inset .composer-bot{
|
||||
margin-top:0; padding:0 10px 10px; justify-content:flex-start;
|
||||
}
|
||||
.page[data-page="collab"] .cdb-inset .composer-bot .spacer{ flex:1; }
|
||||
.page[data-page="collab"] .cdb-plus-wrap{ position:relative; flex:0 0 auto; }
|
||||
.page[data-page="collab"] .cdb-plus-wrap .cdb-menu{
|
||||
left:0; right:auto; top:auto; bottom:calc(100% + 6px); min-width:180px;
|
||||
}
|
||||
.page[data-page="collab"] .cdb-plus{ width:34px; height:34px; }
|
||||
.page[data-page="collab"] .cdb-inset .composer-bot > #cdb-model-chip{ border-radius:4px; }
|
||||
@media (max-width:900px){
|
||||
.collab-guide-steps{ grid-template-columns:repeat(2, minmax(0, 1fr)); gap:40px 32px; }
|
||||
.collab-guide-step{ gap:20px; }
|
||||
|
||||
@@ -86,6 +86,27 @@ class Session:
|
||||
last_error: str = ""
|
||||
pinned: bool = False
|
||||
untitled: bool = True
|
||||
plan_scan_baseline: int = 0
|
||||
plan_path: str = ""
|
||||
|
||||
|
||||
def _load_plan_baseline(item: dict, msgs: list) -> int:
|
||||
"""Persisted per-session baseline (tuiapp_v2: set on /continue, not on preset text)."""
|
||||
base = int(item.get("plan_scan_baseline", 0) or 0)
|
||||
if base >= len(msgs):
|
||||
return 0
|
||||
return max(0, base)
|
||||
|
||||
|
||||
def _sanitize_desktop_plan_path(session_id: str, plan_path: str) -> str:
|
||||
"""Desktop: drop shared plan_demo paths so sessions do not read the same file."""
|
||||
import plan_state
|
||||
p = (plan_path or "").strip()
|
||||
if not p:
|
||||
return ""
|
||||
if plan_state.is_session_scoped_plan_path(p, session_id):
|
||||
return p
|
||||
return plan_state.default_session_plan_path(session_id)
|
||||
|
||||
|
||||
class AgentManager:
|
||||
@@ -111,7 +132,9 @@ class AgentManager:
|
||||
arr.append({"id": s.id, "title": s.title, "cwd": s.cwd,
|
||||
"created_at": s.created_at, "updated_at": s.updated_at,
|
||||
"messages": s.messages, "msg_seq": s.msg_seq,
|
||||
"pinned": s.pinned, "untitled": s.untitled})
|
||||
"pinned": s.pinned, "untitled": s.untitled,
|
||||
"plan_scan_baseline": s.plan_scan_baseline,
|
||||
"plan_path": s.plan_path or ""})
|
||||
self._sessions_file.write_text(json.dumps(arr, ensure_ascii=False, default=str), encoding="utf-8")
|
||||
except Exception as e:
|
||||
print(f"[bridge] persist sessions failed: {e}", file=sys.stderr)
|
||||
@@ -122,14 +145,18 @@ class AgentManager:
|
||||
return
|
||||
arr = json.loads(self._sessions_file.read_text(encoding="utf-8"))
|
||||
for item in arr:
|
||||
msgs = item.get("messages", [])
|
||||
sess = Session(id=item["id"], title=item.get("title", "New chat"),
|
||||
cwd=item.get("cwd", self.ga_root),
|
||||
created_at=item.get("created_at", time.time()),
|
||||
updated_at=item.get("updated_at", time.time()),
|
||||
messages=item.get("messages", []),
|
||||
messages=msgs,
|
||||
msg_seq=item.get("msg_seq", 0),
|
||||
pinned=item.get("pinned", False),
|
||||
untitled=item.get("untitled", True),
|
||||
plan_scan_baseline=_load_plan_baseline(item, msgs),
|
||||
plan_path=_sanitize_desktop_plan_path(
|
||||
item["id"], item.get("plan_path") or ""),
|
||||
status="idle", agent=None)
|
||||
self.sessions[sess.id] = sess
|
||||
if self.sessions:
|
||||
@@ -408,6 +435,10 @@ class AgentManager:
|
||||
if image_metas:
|
||||
extra["images"] = image_metas
|
||||
user_msg = self.add_message(sess, "user", prompt, **extra)
|
||||
import plan_state
|
||||
if plan_state.is_plan_preset_prompt(prompt):
|
||||
plan_state.bind_plan_session(sess, prompt)
|
||||
self._persist()
|
||||
sess.status = "running"
|
||||
sess.cancel_requested = False
|
||||
sess.last_error = ""
|
||||
@@ -478,6 +509,8 @@ class AgentManager:
|
||||
# Strip trailing [Info] Final response to user. marker
|
||||
import re as _re
|
||||
full = _re.sub(r'\n*`{5}\n*\[Info\] Final response to user\.\n*`{5}\s*$', '', full)
|
||||
import plan_state
|
||||
plan_state.sync_plan_path_from_text(sess, full, sess.cwd or self.ga_root)
|
||||
self.add_message(sess, "assistant", full)
|
||||
sess.status = "idle"
|
||||
sess.last_error = ""
|
||||
@@ -500,16 +533,29 @@ class AgentManager:
|
||||
msgs = [m for m in sess.messages if int(m.get("id", 0)) > after]
|
||||
if limit > 0:
|
||||
msgs = msgs[-limit:]
|
||||
import plan_state
|
||||
return {
|
||||
"sessionId": sid,
|
||||
"status": sess.status,
|
||||
"messages": msgs,
|
||||
"partial": dict(sess.partial) if sess.partial else None,
|
||||
"plan": plan_state.desktop_plan_payload_from_session(sess, self.ga_root),
|
||||
"msgSeq": sess.msg_seq,
|
||||
"updatedAt": sess.updated_at,
|
||||
"lastError": sess.last_error,
|
||||
}
|
||||
|
||||
def plan_snapshot(self, sid: str) -> dict:
|
||||
with self.lock:
|
||||
sess = self.sessions.get(sid)
|
||||
if not sess:
|
||||
raise web.HTTPNotFound(text=json.dumps({"error": f"session not found: {sid}"}, ensure_ascii=False), content_type="application/json")
|
||||
import plan_state
|
||||
return {
|
||||
"sessionId": sid,
|
||||
"plan": plan_state.desktop_plan_payload_from_session(sess, self.ga_root),
|
||||
}
|
||||
|
||||
def cancel(self, sid: str) -> dict:
|
||||
with self.lock:
|
||||
sess = self.sessions.get(sid)
|
||||
@@ -829,6 +875,18 @@ class ServiceManager:
|
||||
return {"ok": False, "error": item["lastError"] or "start_failed", "service": item}
|
||||
return {"ok": True, "service": item}
|
||||
|
||||
def autostart_extras(self) -> None:
|
||||
"""Auto-start non-IM services on bridge boot. Currently:
|
||||
- reflect/scheduler.py (drives L4 archive cron every 12h).
|
||||
IM services stay manual (need explicit mykey.py config + user opt-in)."""
|
||||
for sid in sorted(set(self._catalog) - set(self._im_catalog)):
|
||||
try:
|
||||
res = self.start_service(sid)
|
||||
tag = "ok" if res.get("ok") else f"fail: {res.get('error')}"
|
||||
except Exception as e:
|
||||
tag = f"exception {type(e).__name__}: {e}"
|
||||
print(f"[autostart] {sid}: {tag}", file=sys.stderr)
|
||||
|
||||
def stop_service(self, sid: str) -> dict:
|
||||
if sid not in self._catalog:
|
||||
raise KeyError(sid)
|
||||
@@ -1042,6 +1100,8 @@ async def patch_session_handler(request):
|
||||
sess.pinned = bool(data["pinned"])
|
||||
if "untitled" in data:
|
||||
sess.untitled = bool(data["untitled"])
|
||||
if "plan_scan_baseline" in data:
|
||||
sess.plan_scan_baseline = int(data["plan_scan_baseline"])
|
||||
sess.updated_at = time.time()
|
||||
manager._persist()
|
||||
return json_ok({"ok": True, "session": manager.snapshot(sess, include_messages=False)})
|
||||
@@ -1079,6 +1139,11 @@ async def restore_handler(request):
|
||||
return json_ok(manager.restore_context(sid))
|
||||
|
||||
|
||||
async def plan_handler(request):
|
||||
sid = request.match_info["sid"]
|
||||
return json_ok(manager.plan_snapshot(sid))
|
||||
|
||||
|
||||
async def path_open_handler(request):
|
||||
data = await read_json(request)
|
||||
kind = data.get("kind", "")
|
||||
@@ -1407,7 +1472,7 @@ async def post_token_history_handler(request):
|
||||
|
||||
|
||||
def create_app():
|
||||
app = web.Application(middlewares=[cors_middleware], client_max_size=1 * 1024 * 1024)
|
||||
app = web.Application(middlewares=[cors_middleware], client_max_size=500 * 1024 * 1024)
|
||||
app.router.add_get("/ws", ws_handler)
|
||||
app.router.add_get("/status", status_handler)
|
||||
app.router.add_get("/config", get_config_handler)
|
||||
@@ -1424,6 +1489,7 @@ def create_app():
|
||||
app.router.add_patch("/session/{sid}", patch_session_handler)
|
||||
app.router.add_post("/session/{sid}/prompt", prompt_handler)
|
||||
app.router.add_get("/session/{sid}/messages", messages_handler)
|
||||
app.router.add_get("/session/{sid}/plan", plan_handler)
|
||||
app.router.add_post("/session/{sid}/cancel", cancel_handler)
|
||||
app.router.add_post("/session/{sid}/restore", restore_handler)
|
||||
app.router.add_post("/path/open", path_open_handler)
|
||||
@@ -1454,6 +1520,7 @@ def create_app():
|
||||
|
||||
async def on_startup(app):
|
||||
hub.loop = asyncio.get_running_loop()
|
||||
services.autostart_extras()
|
||||
|
||||
app.on_startup.append(on_startup)
|
||||
return app
|
||||
|
||||
+156
-1
@@ -20,7 +20,7 @@ Supported task-line shapes (all matched by `extract`):
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import os, re
|
||||
from typing import Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
_DONE_CHARS = set("xX✓✔√☑")
|
||||
# Newline-insert before a bullet stuck to JSON debris (`{"content": "- [ ] …`).
|
||||
@@ -178,3 +178,158 @@ def summary(items: list[tuple[str, str]]) -> tuple[int, int]:
|
||||
|
||||
def is_complete(items: list[tuple[str, str]]) -> bool:
|
||||
return not items or all(st == "done" for _, st in items)
|
||||
|
||||
|
||||
# --- Desktop bridge only (APIs above unchanged) ---
|
||||
_ENTER_PLAN_RE = re.compile(r"""enter_plan_mode\s*\(\s*["']([^"']+)["']""", re.I)
|
||||
|
||||
|
||||
def _msg_content(m) -> str:
|
||||
if isinstance(m, str): return m
|
||||
c = m.get("content") if isinstance(m, dict) else getattr(m, "content", None)
|
||||
return c if isinstance(c, str) else ""
|
||||
|
||||
|
||||
def plan_path_mention_in_messages(messages, start_idx: int = 0) -> Optional[str]:
|
||||
for m in reversed(_slice(messages, start_idx)):
|
||||
text = _msg_content(m)
|
||||
if not text: continue
|
||||
if "enter_plan_mode" in text and (hit := _ENTER_PLAN_RE.search(text)):
|
||||
return hit.group(1).strip().strip("\"'")
|
||||
if "plan.md" in text and (hits := _PATH_RE.findall(text)):
|
||||
return hits[-1].strip().strip("\"'")
|
||||
return None
|
||||
|
||||
|
||||
def _resolve_stashed_at(p: str, root: str) -> Optional[str]:
|
||||
if not p or not root: return None
|
||||
rel = p.lstrip("./\\")
|
||||
cwd = root.rstrip("/\\")
|
||||
for c in (p, os.path.join(cwd, "temp", rel), os.path.join(cwd, rel)):
|
||||
if os.path.isfile(c) and os.path.getsize(c) > 0: return c
|
||||
return None
|
||||
|
||||
|
||||
def _find_path_at(messages, start_idx: int, root: str) -> Optional[str]:
|
||||
for m in reversed(_slice(messages, start_idx)):
|
||||
text = _msg_content(m)
|
||||
if not text or "plan.md" not in text: continue
|
||||
for hit in reversed(_PATH_RE.findall(text)):
|
||||
if p := _resolve_stashed_at(hit.strip().strip("\"'"), root): return p
|
||||
return None
|
||||
|
||||
|
||||
def default_session_plan_path(session_id: str) -> str:
|
||||
sid = (session_id or "sess").replace("/", "_")
|
||||
return f"temp/plan_{sid}/plan.md"
|
||||
|
||||
|
||||
def is_session_scoped_plan_path(path: str, session_id: str) -> bool:
|
||||
"""Desktop: only bind/adopt paths under this session's plan_{id}/ tree."""
|
||||
if not path:
|
||||
return False
|
||||
sid = (session_id or "sess").replace("/", "_")
|
||||
norm = path.replace("\\", "/").lstrip("./")
|
||||
return f"plan_{sid}/" in norm or norm.endswith(f"plan_{sid}/plan.md")
|
||||
|
||||
|
||||
def is_plan_preset_prompt(prompt: str) -> bool:
|
||||
p = (prompt or "").lower()
|
||||
return "plan_sop" in p or "plan 模式" in p or "plan mode" in p
|
||||
|
||||
|
||||
def bind_plan_session(sess: Any, prompt: str = "", path: Optional[str] = None) -> str:
|
||||
"""Bind plan card to this session only (avoids sharing plan_demo/ across sessions)."""
|
||||
rel = (path or "").strip() or default_session_plan_path(getattr(sess, "id", "") or "")
|
||||
sess.plan_path = rel
|
||||
msgs = list(getattr(sess, "messages", []) or [])
|
||||
sess.plan_scan_baseline = len(msgs)
|
||||
return rel
|
||||
|
||||
|
||||
def sync_plan_path_from_text(sess: Any, text: str, root: str) -> None:
|
||||
"""If agent emits enter_plan_mode(...), keep session bound path in sync."""
|
||||
if not text:
|
||||
return
|
||||
m = _ENTER_PLAN_RE.search(text)
|
||||
if not m:
|
||||
return
|
||||
raw = m.group(1).strip().strip("\"'")
|
||||
if not raw:
|
||||
return
|
||||
sess.plan_path = raw.lstrip("./")
|
||||
|
||||
|
||||
def session_plan_active(sess: Any, agent, messages, start_idx: int, root: str) -> bool:
|
||||
"""Desktop: active when this session has a bound plan_path (not global plan_*/ scan)."""
|
||||
bound = (getattr(sess, "plan_path", None) or "").strip()
|
||||
if not bound:
|
||||
return False
|
||||
if _stashed_plan_path(agent):
|
||||
return True
|
||||
if _resolve_stashed_at(bound, root):
|
||||
return True
|
||||
if getattr(sess, "status", "") == "running":
|
||||
return True
|
||||
# Plan preset bound this session — keep placeholder until plan.md appears or path changes
|
||||
if is_session_scoped_plan_path(bound, getattr(sess, "id", "")):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def session_plan_resolve(bound: str, root: str) -> Optional[str]:
|
||||
if not bound:
|
||||
return None
|
||||
return _resolve_stashed_at(bound.strip(), root)
|
||||
|
||||
|
||||
def _desktop_resolve_plan_file(sess: Any, bound: str, root: str, agent, messages, base: int) -> Optional[str]:
|
||||
"""Desktop read path: bound → agent stash → per-session message scan."""
|
||||
path = session_plan_resolve(bound, root)
|
||||
if path:
|
||||
return path
|
||||
if agent and (stash := _stashed_plan_path(agent)):
|
||||
if p := _resolve_stashed_at(stash, root):
|
||||
return p
|
||||
return _find_path_at(messages, base, root)
|
||||
|
||||
|
||||
def desktop_plan_payload_from_session(sess: Any, ga_root: str = "") -> dict:
|
||||
"""Per-session plan card — bound plan_path + tuiapp_v2-style item/placeholder/complete."""
|
||||
raw = list(getattr(sess, "messages", []) or [])
|
||||
base = max(0, int(getattr(sess, "plan_scan_baseline", 0) or 0))
|
||||
if base > len(raw):
|
||||
base = len(raw)
|
||||
root = (getattr(sess, "cwd", None) or ga_root or "").strip()
|
||||
agent = getattr(sess, "agent", None)
|
||||
partial = getattr(sess, "partial", None)
|
||||
if isinstance(partial, dict) and isinstance(partial.get("content"), str):
|
||||
sync_plan_path_from_text(sess, partial["content"], root)
|
||||
sid = getattr(sess, "id", "") or ""
|
||||
if not (getattr(sess, "plan_path", None) or "").strip():
|
||||
mentioned = plan_path_mention_in_messages(raw, base)
|
||||
if mentioned and is_session_scoped_plan_path(mentioned, sid):
|
||||
sess.plan_path = mentioned.lstrip("./")
|
||||
else:
|
||||
return {"active": False}
|
||||
if not session_plan_active(sess, agent, raw, base, root):
|
||||
return {"active": False}
|
||||
bound = getattr(sess, "plan_path", "") or ""
|
||||
path = _desktop_resolve_plan_file(sess, bound, root, agent, raw, base)
|
||||
items = []
|
||||
if path:
|
||||
try:
|
||||
items = [{"content": c, "status": st} for c, st in extract(open(path, encoding="utf-8", errors="replace").read())]
|
||||
except OSError:
|
||||
pass
|
||||
step_msgs = list(raw[base:])
|
||||
if isinstance(p := getattr(sess, "partial", None), dict) and isinstance(c := p.get("content"), str) and c:
|
||||
step_msgs.append({"content": c})
|
||||
step = current_step(step_msgs, start_idx=0)
|
||||
if not items:
|
||||
hp = getattr(sess, "plan_path", "") or _stashed_plan_path(agent) or ""
|
||||
hint = "/".join(hp.replace("\\", "/").rstrip("/").split("/")[-2:]) if hp else "plan.md"
|
||||
return {"active": True, "placeholder": True, "done": 0, "total": 0, "complete": False, "step": step, "pathHint": hint, "items": []}
|
||||
pairs = [(x["content"], x["status"]) for x in items]
|
||||
n_done, n_total = summary(pairs)
|
||||
return {"active": True, "placeholder": False, "done": n_done, "total": n_total, "complete": is_complete(pairs), "step": step, "items": items}
|
||||
Reference in new issue
Block a user