@mjasnikovs/pi-task 0.17.23 → 0.17.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/remote/protocol.d.ts +3 -0
- package/dist/remote/ui-script.js +8 -3
- package/dist/remote/ui-styles.d.ts +1 -1
- package/dist/remote/ui-styles.js +13 -4
- package/dist/shared/child-process.d.ts +5 -0
- package/dist/shared/child-process.js +2 -1
- package/dist/task/auto-commit.d.ts +15 -0
- package/dist/task/auto-commit.js +47 -3
- package/dist/task/auto-orchestrator.d.ts +24 -0
- package/dist/task/auto-orchestrator.js +82 -3
- package/dist/task/final-gate.d.ts +23 -0
- package/dist/task/final-gate.js +143 -0
- package/dist/task/gate-deps.js +49 -15
- package/dist/task/git-state-guard.d.ts +44 -0
- package/dist/task/git-state-guard.js +240 -0
- package/dist/task/orchestrator.d.ts +8 -0
- package/dist/task/orchestrator.js +1 -1
- package/dist/task/phases.d.ts +5 -0
- package/dist/task/phases.js +30 -4
- package/dist/task/task-gates.js +15 -4
- package/dist/task/verify-work.d.ts +12 -0
- package/dist/task/verify-work.js +14 -0
- package/dist/task/widget.js +20 -2
- package/dist/workers/search-extension.d.ts +2 -0
- package/dist/workers/search-extension.js +6 -0
- package/package.json +1 -1
|
@@ -24,6 +24,9 @@ export interface WidgetData {
|
|
|
24
24
|
total?: number;
|
|
25
25
|
/** Pre-formatted elapsed clock, e.g. "2:14". */
|
|
26
26
|
elapsed?: string;
|
|
27
|
+
/** The current action — the latest child/worker output line (the terminal's
|
|
28
|
+
* `↳` trailer). Shown under the widget so the remote isn't blind to progress. */
|
|
29
|
+
action?: string;
|
|
27
30
|
}
|
|
28
31
|
/** The single task-widget slot. `lines: null` clears it. `data` carries the
|
|
29
32
|
* structured view (null when clearing or when a producer only has text). */
|
package/dist/remote/ui-script.js
CHANGED
|
@@ -76,9 +76,12 @@ export function clientScript(wsUrl) {
|
|
|
76
76
|
function renderStructuredWidget(d) {
|
|
77
77
|
statusPanel.classList.add('structured');
|
|
78
78
|
statusPanel.style.display = 'block';
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
// Title on its own line (wraps to 2, never truncated to a stub).
|
|
80
|
+
var title = '<div class="widget-title">' + escHtml(d.title || '') + '</div>';
|
|
81
|
+
// Meta row: phase badge · step count · elapsed clock.
|
|
82
|
+
var meta = '<div class="widget-meta">'
|
|
81
83
|
+ (d.phase ? '<span class="widget-phase">' + escHtml(d.phase) + '</span>' : '')
|
|
84
|
+
+ (d.total > 0 && d.done != null ? '<span class="widget-step">' + d.done + '/' + d.total + '</span>' : '')
|
|
82
85
|
+ (d.elapsed ? '<span class="widget-elapsed">' + escHtml(d.elapsed) + '</span>' : '')
|
|
83
86
|
+ '</div>';
|
|
84
87
|
var bar = '';
|
|
@@ -86,7 +89,9 @@ export function clientScript(wsUrl) {
|
|
|
86
89
|
var pct = Math.max(0, Math.min(100, Math.round((d.done / d.total) * 100)));
|
|
87
90
|
bar = '<div class="widget-bar"><div class="widget-bar-fill" style="width:' + pct + '%"></div></div>';
|
|
88
91
|
}
|
|
89
|
-
|
|
92
|
+
// Current action — the terminal's last worker/child line, one ellipsized line.
|
|
93
|
+
var action = d.action ? '<div class="widget-action">↳ ' + escHtml(d.action) + '</div>' : '';
|
|
94
|
+
statusPanel.innerHTML = title + meta + bar + action;
|
|
90
95
|
}
|
|
91
96
|
const promptCard = document.getElementById('prompt-card');
|
|
92
97
|
const promptQ = document.getElementById('prompt-q');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const STYLES = " :root {\n --base: #1e1e2e; --mantle: #181825; --crust: #11111b;\n --surface0: #313244; --surface1: #45475a; --surface2: #585b70;\n --text: #cdd6f4; --subtext1: #a6adc8; --subtext0: #7f849c;\n --mauve: #cba6f7; --blue: #89b4fa; --green: #a6e3a1; --red: #f38ba8;\n --yellow: #f9e2af; --peach: #fab387; --teal: #94e2d5;\n }\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n background: var(--base); color: var(--text);\n font-family: ui-monospace, monospace;\n /* --app-h is set from window.innerHeight (see setAppHeight) so the column\n height is a stable pixel value across an orientation change. 100dvh is a\n fallback for first paint / no-JS: iOS Safari interpolates dvh during the\n rotation animation, which makes the whole flex column resize repeatedly\n (\"spazzing out\") \u2014 a fixed px height does not. */\n height: var(--app-h, 100dvh);\n display: flex; flex-direction: column; overflow: hidden;\n padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px)\n 0px env(safe-area-inset-left, 0px);\n }\n #context-bar { height: 4px; background: var(--surface0); flex-shrink: 0; }\n #context-bar-fill { height: 100%; background: var(--mauve); width: 0%; transition: width 0.4s ease; }\n #header {\n background: var(--mantle); padding: 8px 16px;\n display: flex; justify-content: space-between; align-items: center;\n font-size: 13px; flex-shrink: 0; border-bottom: 1px solid var(--surface0);\n }\n #header .title { font-weight: bold; color: var(--mauve); letter-spacing: 0.05em;\n position: relative; animation: glitch 5s steps(1) infinite; }\n @keyframes glitch {\n 0%, 88%, 100% { text-shadow: none; transform: translate(0, 0); }\n 90% { text-shadow: -1px 0 var(--red), 1px 0 var(--teal); transform: translate(1px, -1px); }\n 92% { text-shadow: 1px 0 var(--red), -1px 0 var(--blue); transform: translate(-1px, 1px); }\n 94% { text-shadow: -1px 0 var(--blue), 1px 0 var(--red); transform: translate(1px, 0); }\n 96% { text-shadow: 1px 0 var(--teal), -1px 0 var(--red); transform: translate(-1px, 0); }\n }\n @media (prefers-reduced-motion: reduce) { #header .title { animation: none; } }\n #header .hgroup { display: flex; align-items: center; gap: 10px; }\n #bell {\n background: none; border: none; color: var(--subtext1); cursor: pointer;\n font-size: 15px; line-height: 1; padding: 2px; font-family: inherit;\n }\n #bell:hover { color: var(--text); }\n #bell.on { color: var(--mauve); }\n /* Header status chip: connection dot + model name + context usage. */\n #status-chip { display: flex; align-items: center; gap: 7px; font-size: 11px; color: var(--subtext0); }\n #status-dot {\n width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;\n background: var(--surface2); transition: background 0.2s ease;\n }\n #status-dot.idle { background: var(--green); }\n #status-dot.running { background: var(--mauve); animation: dot-pulse 1.2s ease-in-out infinite; }\n #status-dot.disconnected { background: var(--red); }\n @keyframes dot-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }\n @media (prefers-reduced-motion: reduce) { #status-dot.running { animation: none; } }\n #status-model { color: var(--subtext1); }\n #status-model:empty { display: none; }\n #status-ctx { color: var(--subtext0); font-variant-numeric: tabular-nums; }\n #status-ctx:empty { display: none; }\n /* Notification bell dropdown: a push toggle row + the recent-toast history. */\n #notif-panel {\n display: none; position: fixed; z-index: 80;\n top: calc(env(safe-area-inset-top, 0px) + 42px);\n right: calc(env(safe-area-inset-right, 0px) + 12px);\n width: min(320px, calc(100vw - 24px));\n background: var(--mantle); border: 1px solid var(--surface1); border-radius: 8px;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); overflow: hidden;\n }\n #notif-panel.open { display: block; }\n #notif-toggle-row {\n display: flex; align-items: center; justify-content: space-between;\n padding: 8px 12px; border-bottom: 1px solid var(--surface0);\n }\n #notif-title { font-size: 12px; color: var(--subtext1); font-weight: 700; }\n #notif-toggle {\n background: var(--surface1); color: var(--text); border: none; border-radius: 6px;\n padding: 4px 10px; font-family: inherit; font-size: 11px; cursor: pointer;\n }\n #notif-toggle:hover { filter: brightness(1.1); }\n #notif-toggle.on { background: var(--mauve); color: var(--crust); font-weight: 700; }\n #notif-list { max-height: 40dvh; overflow-y: auto; }\n #notif-empty { padding: 14px 12px; color: var(--subtext0); font-size: 11px; text-align: center; }\n .notif-item {\n display: flex; align-items: baseline; gap: 8px; padding: 7px 12px;\n border-bottom: 1px solid var(--surface0); font-size: 12px;\n }\n .notif-item:last-child { border-bottom: none; }\n .notif-item .notif-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; background: var(--blue); align-self: center; }\n .notif-item.warning .notif-dot { background: var(--peach); }\n .notif-item.error .notif-dot { background: var(--red); }\n .notif-item .notif-msg { flex: 1; min-width: 0; color: var(--text); overflow-wrap: anywhere; word-break: break-word; }\n .notif-item .notif-time { flex-shrink: 0; color: var(--subtext0); font-size: 10px; font-variant-numeric: tabular-nums; }\n #chat-wrap { position: relative; flex: 1; min-height: 0; display: flex; }\n #chat-log {\n flex: 1; min-width: 0; overflow-y: auto; overflow-x: hidden; padding: 16px;\n display: flex; flex-direction: column; gap: 8px;\n }\n /* Floating jump-to-latest button \u2014 only shown when scrolled away from the\n bottom (toggled via .visible from the scroll handler). */\n #scroll-bottom {\n display: none; position: absolute; bottom: 16px; right: 16px; z-index: 40;\n width: 36px; height: 36px; border-radius: 50%; cursor: pointer;\n background: var(--surface1); color: var(--text); border: 1px solid var(--surface2);\n font-family: inherit; font-size: 18px; line-height: 1; padding: 0;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n }\n #scroll-bottom:hover { background: var(--surface2); color: var(--mauve); }\n #scroll-bottom.visible { display: block; }\n #chat-log::-webkit-scrollbar { width: 6px; }\n #chat-log::-webkit-scrollbar-track { background: transparent; }\n #chat-log::-webkit-scrollbar-thumb { background: var(--surface2); border-radius: 3px; }\n .bubble {\n max-width: 82%; padding: 8px 12px; border-radius: 8px;\n line-height: 1.6; white-space: pre-wrap; word-break: break-word; font-size: 13px;\n }\n .bubble.user { background: var(--surface1); color: var(--text); align-self: flex-end; }\n .bubble.assistant { background: var(--surface0); color: var(--text); align-self: flex-start; position: relative; }\n .bubble.error {\n background: var(--crust); color: var(--red); align-self: stretch;\n max-width: 100%; border: 1px solid var(--red); font-size: 12px;\n }\n /* Persistent inline system note (e.g. context compaction) \u2014 a muted centered\n divider, distinct from chat bubbles. */\n .sysnote {\n align-self: center; color: var(--subtext0); font-size: 11px;\n font-family: ui-monospace, monospace; letter-spacing: 0.5px;\n padding: 2px 10px; opacity: 0.85;\n }\n .bubble.thinking {\n display: flex; gap: 5px; align-items: center; padding: 10px 14px;\n }\n .bubble.thinking .spinner {\n color: var(--mauve); font-size: 15px; line-height: 1;\n font-family: ui-monospace, monospace;\n }\n /* Collapsed reasoning block (\"\u273B Thinking\u2026 (n lines)\"), muted + italic. */\n .thinking-block { align-self: flex-start; max-width: 90%; font-size: 12px; }\n .thinking-block > summary {\n color: var(--subtext0); font-style: italic; cursor: pointer; list-style: none;\n user-select: none; display: flex; align-items: center; gap: 8px; padding: 2px 0;\n }\n .thinking-block > summary::-webkit-details-marker { display: none; }\n .thinking-block .thinking-spin {\n color: var(--mauve); font-style: normal; font-family: ui-monospace, monospace;\n }\n .thinking-block .thinking-body {\n color: var(--subtext0); font-style: italic; white-space: pre-wrap;\n word-break: break-word; line-height: 1.5; margin: 4px 0 0 4px;\n padding: 4px 0 2px 12px; border-left: 2px solid var(--surface1);\n }\n /* Copy buttons: on code-block headers and (floating) on finished assistant\n bubbles. Wired by one delegated click handler in the client script. */\n .copy-btn {\n background: transparent; border: none; color: var(--subtext0); cursor: pointer;\n font-family: inherit; font-size: 11px; padding: 2px 6px; border-radius: 4px;\n line-height: 1.4;\n }\n .copy-btn:hover { color: var(--text); background: var(--surface1); }\n .copy-btn.copied { color: var(--green); }\n .bubble-copy {\n position: absolute; top: 4px; right: 4px; opacity: 0;\n background: var(--surface1); transition: opacity 0.12s ease;\n }\n .bubble.assistant:hover .bubble-copy { opacity: 1; }\n /* Touch devices have no hover \u2014 keep the button faintly visible. */\n @media (hover: none) { .bubble-copy { opacity: 0.55; } }\n .tool-call {\n background: var(--crust); border-radius: 6px; align-self: flex-start;\n max-width: 90%; font-size: 12px; border: 1px solid var(--surface0);\n }\n .tool-call summary {\n padding: 6px 10px; color: var(--subtext1); cursor: pointer;\n user-select: none; list-style: none;\n display: flex; align-items: center; gap: 8px;\n }\n .tool-call summary::-webkit-details-marker { display: none; }\n .tool-call summary::before { content: \"\u25B6\"; flex-shrink: 0; font-size: 9px; color: var(--subtext0); }\n .tool-call[open] > summary::before { content: \"\u25BC\"; }\n .tool-call.error > summary { color: var(--red); }\n /* The summary is a single line: the label ellipsizes (full text in the title\n tooltip / on expand), badges and timing stay pinned to the right. */\n .tool-label {\n flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n font-family: ui-monospace, monospace;\n }\n .tool-badge { flex-shrink: 0; color: var(--subtext0); font-size: 11px; }\n .tool-elapsed { flex-shrink: 0; color: var(--subtext0); font-size: 11px; }\n .tool-call pre {\n padding: 8px 12px; overflow-y: auto;\n color: var(--subtext1); font-size: 11px; max-height: 280px;\n border-top: 1px solid var(--surface0);\n white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word;\n }\n .tool-spin { color: var(--mauve); flex-shrink: 0; font-family: ui-monospace, monospace; font-size: 13px; }\n /* Line diff for edit/write tools (tints derived from --green/--red). */\n .tool-diff { border-top: 1px solid var(--surface0); overflow-x: auto; }\n .diff { font-family: ui-monospace, monospace; font-size: 11px; padding: 4px 0; }\n .diff-line { white-space: pre; padding: 0 10px; }\n .diff-sign { display: inline-block; width: 1ch; margin-right: 8px; color: var(--subtext0); }\n .diff-add { background: color-mix(in srgb, var(--green) 14%, transparent); color: var(--green); }\n .diff-del { background: color-mix(in srgb, var(--red) 14%, transparent); color: var(--red); }\n .diff-ctx { color: var(--subtext1); }\n .code-block {\n background: var(--crust); border: 1px solid var(--surface0);\n border-radius: 6px; overflow: hidden; margin: 4px 0;\n align-self: stretch; max-width: 100%; font-size: 12px;\n }\n /* Header row above a code block: language label on the left, copy button on\n the right (the surface bar that used to live on .code-lang). */\n .code-head {\n display: flex; align-items: center; justify-content: space-between;\n background: var(--surface0);\n }\n .code-lang {\n color: var(--subtext0); font-size: 10px; padding: 3px 10px; letter-spacing: 0.05em;\n }\n .code-head .copy-btn { padding: 3px 10px; }\n .code-block code {\n display: block; padding: 10px 12px; overflow-x: auto;\n color: var(--text); white-space: pre; line-height: 1.55;\n }\n .hl-kw { color: var(--mauve); }\n .hl-str { color: var(--green); }\n .hl-cmt { color: var(--subtext0); font-style: italic; }\n .hl-num { color: var(--blue); }\n .hl-fn { color: var(--yellow); }\n /* Hand-rolled markdown, applied only to assistant bubbles + the recommendation\n panel (both get the .md class from setContent). Block elements lay themselves\n out, so switch off the container's pre-wrap for these. */\n .md { white-space: normal; }\n .md > :first-child { margin-top: 0; }\n .md > :last-child { margin-bottom: 0; }\n .md .md-h { color: var(--mauve); font-weight: 700; line-height: 1.3; margin: 10px 0 4px; }\n .md .md-h1 { font-size: 1.35em; }\n .md .md-h2 { font-size: 1.2em; }\n .md .md-h3 { font-size: 1.08em; }\n .md .md-h4, .md .md-h5, .md .md-h6 { font-size: 1em; }\n .md .md-p { margin: 6px 0; }\n .md .md-list { margin: 6px 0; padding-left: 22px; }\n .md .md-list li { margin: 2px 0; }\n .md .md-task { list-style: none; margin-left: -22px; }\n .md .md-check { color: var(--green); }\n .md .md-quote { border-left: 3px solid var(--surface2); margin: 6px 0; padding: 2px 0 2px 10px; color: var(--subtext1); }\n .md .md-hr { border: none; border-top: 1px solid var(--surface1); margin: 10px 0; }\n .md a { color: var(--blue); text-decoration: underline; }\n .md strong { color: var(--text); font-weight: 700; }\n .md em { font-style: italic; }\n .md del { color: var(--subtext0); }\n .md .md-code {\n background: var(--crust); border: 1px solid var(--surface0);\n border-radius: 4px; padding: 1px 4px; font-size: 0.92em;\n }\n .md .md-table { border-collapse: collapse; margin: 8px 0; font-size: 0.95em; display: block; overflow-x: auto; }\n .md .md-table th, .md .md-table td { border: 1px solid var(--surface1); padding: 4px 8px; text-align: left; }\n .md .md-table th { background: var(--surface0); color: var(--subtext1); }\n #input-bar {\n background: var(--mantle); padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));\n display: flex; gap: 8px; flex-shrink: 0;\n border-top: 1px solid var(--surface0);\n position: relative;\n }\n #cmd-suggestions {\n display: none; position: absolute; bottom: 100%; left: 16px; right: 16px;\n background: var(--mantle); border: 1px solid var(--surface1);\n border-bottom: none; border-radius: 8px 8px 0 0;\n overflow: hidden; z-index: 10;\n }\n .cmd-item {\n display: flex; align-items: baseline; gap: 10px;\n padding: 7px 12px; cursor: pointer; font-size: 12px;\n border-bottom: 1px solid var(--surface0);\n }\n .cmd-item:last-child { border-bottom: none; }\n .cmd-item:hover, .cmd-item.active { background: var(--surface0); }\n .cmd-item .cmd-name { color: var(--blue); font-weight: bold; flex-shrink: 0; }\n .cmd-item .cmd-desc { color: var(--subtext0); }\n #input {\n flex: 1; background: var(--surface0); color: var(--text);\n border: none; border-radius: 6px; padding: 8px 12px;\n font-family: inherit; font-size: 13px; resize: none;\n outline: none; line-height: 1.5; min-height: 36px; max-height: 120px;\n }\n #input::placeholder { color: var(--subtext0); }\n #input:focus { box-shadow: 0 0 0 1px var(--mauve); }\n #send {\n background: var(--blue); color: var(--crust); border: none;\n border-radius: 6px; padding: 8px 16px; font-weight: bold;\n cursor: pointer; font-size: 13px; font-family: inherit;\n white-space: nowrap; align-self: flex-end;\n }\n #send:disabled, #input:disabled { opacity: 0.45; cursor: not-allowed; }\n /* While the agent runs, Send becomes a red Stop; the armed (tap-to-confirm)\n state brightens it and adds a halo, mirroring the prompt card's cancel. */\n #send.stop { background: var(--red); color: var(--crust); }\n #send.stop.armed {\n filter: brightness(1.12);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--red) 45%, transparent);\n }\n #reconnect-overlay {\n display: none; position: fixed; inset: 0;\n background: rgba(30,30,46,0.88); color: var(--subtext1);\n justify-content: center; align-items: center;\n font-size: 13px; z-index: 100; letter-spacing: 0.03em;\n }\n #reconnect-overlay.visible { display: flex; }\n /* Trailing stream indicator: the same braille spinner as the thinking bubble,\n inline at the end of the streaming text (not a green blinking block). */\n .cursor {\n color: var(--mauve); margin-left: 2px;\n font-family: ui-monospace, monospace;\n }\n #status-panel { padding: 6px 12px; border-bottom: 1px solid var(--surface1);\n color: var(--subtext1); white-space: pre-wrap; font-size: 13px; display: none; }\n /* Structured task widget (progress bar + phase badge + elapsed). Replaces the\n plain text lines when the server sends a structured data payload. */\n #status-panel.structured { white-space: normal; display: block; }\n .widget-top { display: flex; align-items: center; gap: 8px; }\n .widget-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;\n white-space: nowrap; color: var(--text); font-size: 13px; }\n .widget-phase { flex-shrink: 0; background: var(--surface0); color: var(--mauve);\n border-radius: 4px; padding: 1px 7px; font-size: 10px; letter-spacing: 0.03em;\n text-transform: uppercase; }\n .widget-elapsed { flex-shrink: 0; color: var(--subtext0); font-size: 11px;\n font-variant-numeric: tabular-nums; }\n .widget-bar { height: 4px; background: var(--surface0); border-radius: 2px;\n overflow: hidden; margin-top: 6px; }\n .widget-bar-fill { height: 100%; background: var(--mauve); border-radius: 2px;\n transition: width 0.3s ease; }\n /* Dim per-turn timestamp shown under a committed turn's bubble. */\n .turn-time { font-size: 10px; color: var(--subtext0); opacity: 0.55; padding: 0 4px;\n margin-top: -2px; }\n .turn-time.user { align-self: flex-end; }\n .turn-time.assistant, .turn-time.system { align-self: flex-start; }\n .turn-time.system { align-self: center; }\n #prompt-card { position: fixed; left: 0; right: 0; bottom: 0; background: var(--mantle);\n border-top: 2px solid var(--mauve); padding: 16px 14px calc(16px + env(safe-area-inset-bottom, 0px));\n display: none; z-index: 50; max-height: 80dvh; overflow-y: auto; }\n #prompt-card .q-label { color: var(--mauve); font-size: 11px; font-weight: 700;\n text-transform: uppercase; letter-spacing: .6px; margin-bottom: 6px; }\n #prompt-card .q { color: var(--text); margin-bottom: 12px; white-space: pre-wrap;\n font-size: 15px; line-height: 1.5; }\n #prompt-card .rec-panel { background: var(--surface0); border-left: 3px solid var(--green);\n border-radius: 6px; padding: 10px 12px; margin-bottom: 12px; }\n #prompt-card .rec-label { color: var(--green); font-size: 11px; font-weight: 700;\n text-transform: uppercase; letter-spacing: .5px; margin-bottom: 4px; }\n #prompt-card .rec-text { color: var(--text); font-size: 15px; line-height: 1.5;\n white-space: pre-wrap; overflow-wrap: anywhere; }\n #prompt-card textarea { width: 100%; background: var(--surface0); color: var(--text);\n border: 1px solid var(--surface2); border-radius: 6px; padding: 10px; font-size: 15px;\n font-family: inherit; line-height: 1.5; resize: vertical; margin-bottom: 4px; }\n #prompt-card .row { display: flex; gap: 8px; margin-top: 12px; align-items: stretch;\n flex-wrap: wrap; }\n /* Recommendation answers can be long sentences, so stack them as a readable list. */\n #prompt-card .row.stacked { flex-direction: column; align-items: stretch; }\n #prompt-card .row.stacked button { flex: none; text-align: left; }\n #prompt-card .row.stacked button.cancel { align-self: center; text-align: center; }\n #prompt-card button { padding: 11px 16px; border-radius: 8px; border: none; cursor: pointer;\n font-family: inherit; font-size: 14px; font-weight: 600; transition: filter .15s ease; }\n #prompt-card button:hover { filter: brightness(1.08); }\n #prompt-card button.primary { background: var(--green); color: var(--crust);\n font-weight: 700; flex: 1; min-width: 160px; }\n #prompt-card button.secondary { background: var(--surface1); color: var(--text);\n flex: 1; min-width: 160px; }\n #prompt-card button.cancel { margin-left: auto; align-self: center; background: transparent;\n color: var(--subtext0); font-size: 12px; font-weight: 500; padding: 8px 10px; }\n #prompt-card button.cancel:hover { color: var(--red); filter: none; }\n #prompt-card button.cancel.armed { background: var(--red); color: var(--crust); font-weight: 700; }\n .toast { position: fixed; top: calc(env(safe-area-inset-top, 0px) + 12px);\n right: calc(env(safe-area-inset-right, 0px) + 12px); max-width: calc(100vw - 24px);\n padding: 8px 12px; border-radius: 6px; overflow-wrap: anywhere; word-break: break-word;\n background: var(--surface1); color: var(--text); z-index: 60; }\n .toast.warning { background: var(--peach); color: var(--crust); }\n .toast.error { background: var(--red); color: var(--crust); }\n #viewer { position: fixed; inset: 24px; background: var(--mantle); border: 1px solid var(--surface2);\n border-radius: 8px; padding: 16px; overflow: auto; white-space: pre-wrap;\n overflow-wrap: anywhere; word-break: break-word; display: none; z-index: 70; }\n #viewer .close { position: absolute; top: 8px; right: 12px; cursor: pointer; color: var(--subtext0); }\n /* Desktop: center the transcript/status/input in a readable column instead of\n hugging the left edge. The scrollbar stays at the true window edge; only the\n content is inset. Mobile (below 960px) is unchanged. */\n @media (min-width: 960px) {\n #chat-log { padding-left: calc((100% - 920px) / 2); padding-right: calc((100% - 920px) / 2); }\n #status-panel { padding-left: calc((100% - 920px) / 2 + 12px); padding-right: calc((100% - 920px) / 2 + 12px); }\n #input-bar { padding-left: calc((100% - 920px) / 2); padding-right: calc((100% - 920px) / 2); }\n #cmd-suggestions { left: calc((100% - 920px) / 2 + 16px); right: calc((100% - 920px) / 2 + 16px); }\n }";
|
|
1
|
+
export declare const STYLES = " :root {\n --base: #1e1e2e; --mantle: #181825; --crust: #11111b;\n --surface0: #313244; --surface1: #45475a; --surface2: #585b70;\n --text: #cdd6f4; --subtext1: #a6adc8; --subtext0: #7f849c;\n --mauve: #cba6f7; --blue: #89b4fa; --green: #a6e3a1; --red: #f38ba8;\n --yellow: #f9e2af; --peach: #fab387; --teal: #94e2d5;\n }\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n background: var(--base); color: var(--text);\n font-family: ui-monospace, monospace;\n /* --app-h is set from window.innerHeight (see setAppHeight) so the column\n height is a stable pixel value across an orientation change. 100dvh is a\n fallback for first paint / no-JS: iOS Safari interpolates dvh during the\n rotation animation, which makes the whole flex column resize repeatedly\n (\"spazzing out\") \u2014 a fixed px height does not. */\n height: var(--app-h, 100dvh);\n display: flex; flex-direction: column; overflow: hidden;\n padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px)\n 0px env(safe-area-inset-left, 0px);\n }\n #context-bar { height: 4px; background: var(--surface0); flex-shrink: 0; }\n #context-bar-fill { height: 100%; background: var(--mauve); width: 0%; transition: width 0.4s ease; }\n #header {\n background: var(--mantle); padding: 8px 16px;\n display: flex; justify-content: space-between; align-items: center;\n font-size: 13px; flex-shrink: 0; border-bottom: 1px solid var(--surface0);\n }\n #header .title { font-weight: bold; color: var(--mauve); letter-spacing: 0.05em;\n position: relative; animation: glitch 5s steps(1) infinite; }\n @keyframes glitch {\n 0%, 88%, 100% { text-shadow: none; transform: translate(0, 0); }\n 90% { text-shadow: -1px 0 var(--red), 1px 0 var(--teal); transform: translate(1px, -1px); }\n 92% { text-shadow: 1px 0 var(--red), -1px 0 var(--blue); transform: translate(-1px, 1px); }\n 94% { text-shadow: -1px 0 var(--blue), 1px 0 var(--red); transform: translate(1px, 0); }\n 96% { text-shadow: 1px 0 var(--teal), -1px 0 var(--red); transform: translate(-1px, 0); }\n }\n @media (prefers-reduced-motion: reduce) { #header .title { animation: none; } }\n #header .hgroup { display: flex; align-items: center; gap: 10px; }\n #bell {\n background: none; border: none; color: var(--subtext1); cursor: pointer;\n font-size: 15px; line-height: 1; padding: 2px; font-family: inherit;\n }\n #bell:hover { color: var(--text); }\n #bell.on { color: var(--mauve); }\n /* Header status chip: connection dot + model name + context usage. */\n #status-chip { display: flex; align-items: center; gap: 7px; font-size: 11px; color: var(--subtext0); }\n #status-dot {\n width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;\n background: var(--surface2); transition: background 0.2s ease;\n }\n #status-dot.idle { background: var(--green); }\n #status-dot.running { background: var(--mauve); animation: dot-pulse 1.2s ease-in-out infinite; }\n #status-dot.disconnected { background: var(--red); }\n @keyframes dot-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }\n @media (prefers-reduced-motion: reduce) { #status-dot.running { animation: none; } }\n #status-model { color: var(--subtext1); }\n #status-model:empty { display: none; }\n #status-ctx { color: var(--subtext0); font-variant-numeric: tabular-nums; }\n #status-ctx:empty { display: none; }\n /* Notification bell dropdown: a push toggle row + the recent-toast history. */\n #notif-panel {\n display: none; position: fixed; z-index: 80;\n top: calc(env(safe-area-inset-top, 0px) + 42px);\n right: calc(env(safe-area-inset-right, 0px) + 12px);\n width: min(320px, calc(100vw - 24px));\n background: var(--mantle); border: 1px solid var(--surface1); border-radius: 8px;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); overflow: hidden;\n }\n #notif-panel.open { display: block; }\n #notif-toggle-row {\n display: flex; align-items: center; justify-content: space-between;\n padding: 8px 12px; border-bottom: 1px solid var(--surface0);\n }\n #notif-title { font-size: 12px; color: var(--subtext1); font-weight: 700; }\n #notif-toggle {\n background: var(--surface1); color: var(--text); border: none; border-radius: 6px;\n padding: 4px 10px; font-family: inherit; font-size: 11px; cursor: pointer;\n }\n #notif-toggle:hover { filter: brightness(1.1); }\n #notif-toggle.on { background: var(--mauve); color: var(--crust); font-weight: 700; }\n #notif-list { max-height: 40dvh; overflow-y: auto; }\n #notif-empty { padding: 14px 12px; color: var(--subtext0); font-size: 11px; text-align: center; }\n .notif-item {\n display: flex; align-items: baseline; gap: 8px; padding: 7px 12px;\n border-bottom: 1px solid var(--surface0); font-size: 12px;\n }\n .notif-item:last-child { border-bottom: none; }\n .notif-item .notif-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; background: var(--blue); align-self: center; }\n .notif-item.warning .notif-dot { background: var(--peach); }\n .notif-item.error .notif-dot { background: var(--red); }\n .notif-item .notif-msg { flex: 1; min-width: 0; color: var(--text); overflow-wrap: anywhere; word-break: break-word; }\n .notif-item .notif-time { flex-shrink: 0; color: var(--subtext0); font-size: 10px; font-variant-numeric: tabular-nums; }\n #chat-wrap { position: relative; flex: 1; min-height: 0; display: flex; }\n #chat-log {\n flex: 1; min-width: 0; overflow-y: auto; overflow-x: hidden; padding: 16px;\n display: flex; flex-direction: column; gap: 8px;\n }\n /* Floating jump-to-latest button \u2014 only shown when scrolled away from the\n bottom (toggled via .visible from the scroll handler). */\n #scroll-bottom {\n display: none; position: absolute; bottom: 16px; right: 16px; z-index: 40;\n width: 36px; height: 36px; border-radius: 50%; cursor: pointer;\n background: var(--surface1); color: var(--text); border: 1px solid var(--surface2);\n font-family: inherit; font-size: 18px; line-height: 1; padding: 0;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n }\n #scroll-bottom:hover { background: var(--surface2); color: var(--mauve); }\n #scroll-bottom.visible { display: block; }\n #chat-log::-webkit-scrollbar { width: 6px; }\n #chat-log::-webkit-scrollbar-track { background: transparent; }\n #chat-log::-webkit-scrollbar-thumb { background: var(--surface2); border-radius: 3px; }\n .bubble {\n max-width: 82%; padding: 8px 12px; border-radius: 8px;\n line-height: 1.6; white-space: pre-wrap; word-break: break-word; font-size: 13px;\n }\n .bubble.user { background: var(--surface1); color: var(--text); align-self: flex-end; }\n .bubble.assistant { background: var(--surface0); color: var(--text); align-self: flex-start; position: relative; }\n .bubble.error {\n background: var(--crust); color: var(--red); align-self: stretch;\n max-width: 100%; border: 1px solid var(--red); font-size: 12px;\n }\n /* Persistent inline system note (e.g. context compaction) \u2014 a muted centered\n divider, distinct from chat bubbles. */\n .sysnote {\n align-self: center; color: var(--subtext0); font-size: 11px;\n font-family: ui-monospace, monospace; letter-spacing: 0.5px;\n padding: 2px 10px; opacity: 0.85;\n }\n .bubble.thinking {\n display: flex; gap: 5px; align-items: center; padding: 10px 14px;\n }\n .bubble.thinking .spinner {\n color: var(--mauve); font-size: 15px; line-height: 1;\n font-family: ui-monospace, monospace;\n }\n /* Collapsed reasoning block (\"\u273B Thinking\u2026 (n lines)\"), muted + italic. */\n .thinking-block { align-self: flex-start; max-width: 90%; font-size: 12px; }\n .thinking-block > summary {\n color: var(--subtext0); font-style: italic; cursor: pointer; list-style: none;\n user-select: none; display: flex; align-items: center; gap: 8px; padding: 2px 0;\n }\n .thinking-block > summary::-webkit-details-marker { display: none; }\n .thinking-block .thinking-spin {\n color: var(--mauve); font-style: normal; font-family: ui-monospace, monospace;\n }\n .thinking-block .thinking-body {\n color: var(--subtext0); font-style: italic; white-space: pre-wrap;\n word-break: break-word; line-height: 1.5; margin: 4px 0 0 4px;\n padding: 4px 0 2px 12px; border-left: 2px solid var(--surface1);\n }\n /* Copy buttons: on code-block headers and (floating) on finished assistant\n bubbles. Wired by one delegated click handler in the client script. */\n .copy-btn {\n background: transparent; border: none; color: var(--subtext0); cursor: pointer;\n font-family: inherit; font-size: 11px; padding: 2px 6px; border-radius: 4px;\n line-height: 1.4;\n }\n .copy-btn:hover { color: var(--text); background: var(--surface1); }\n .copy-btn.copied { color: var(--green); }\n .bubble-copy {\n position: absolute; top: 4px; right: 4px; opacity: 0;\n background: var(--surface1); transition: opacity 0.12s ease;\n }\n .bubble.assistant:hover .bubble-copy { opacity: 1; }\n /* Touch devices have no hover \u2014 keep the button faintly visible. */\n @media (hover: none) { .bubble-copy { opacity: 0.55; } }\n .tool-call {\n background: var(--crust); border-radius: 6px; align-self: flex-start;\n max-width: 90%; font-size: 12px; border: 1px solid var(--surface0);\n }\n .tool-call summary {\n padding: 6px 10px; color: var(--subtext1); cursor: pointer;\n user-select: none; list-style: none;\n display: flex; align-items: center; gap: 8px;\n }\n .tool-call summary::-webkit-details-marker { display: none; }\n .tool-call summary::before { content: \"\u25B6\"; flex-shrink: 0; font-size: 9px; color: var(--subtext0); }\n .tool-call[open] > summary::before { content: \"\u25BC\"; }\n .tool-call.error > summary { color: var(--red); }\n /* The summary is a single line: the label ellipsizes (full text in the title\n tooltip / on expand), badges and timing stay pinned to the right. */\n .tool-label {\n flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n font-family: ui-monospace, monospace;\n }\n .tool-badge { flex-shrink: 0; color: var(--subtext0); font-size: 11px; }\n .tool-elapsed { flex-shrink: 0; color: var(--subtext0); font-size: 11px; }\n .tool-call pre {\n padding: 8px 12px; overflow-y: auto;\n color: var(--subtext1); font-size: 11px; max-height: 280px;\n border-top: 1px solid var(--surface0);\n white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word;\n }\n .tool-spin { color: var(--mauve); flex-shrink: 0; font-family: ui-monospace, monospace; font-size: 13px; }\n /* Line diff for edit/write tools (tints derived from --green/--red). */\n .tool-diff { border-top: 1px solid var(--surface0); overflow-x: auto; }\n .diff { font-family: ui-monospace, monospace; font-size: 11px; padding: 4px 0; }\n .diff-line { white-space: pre; padding: 0 10px; }\n .diff-sign { display: inline-block; width: 1ch; margin-right: 8px; color: var(--subtext0); }\n .diff-add { background: color-mix(in srgb, var(--green) 14%, transparent); color: var(--green); }\n .diff-del { background: color-mix(in srgb, var(--red) 14%, transparent); color: var(--red); }\n .diff-ctx { color: var(--subtext1); }\n .code-block {\n background: var(--crust); border: 1px solid var(--surface0);\n border-radius: 6px; overflow: hidden; margin: 4px 0;\n align-self: stretch; max-width: 100%; font-size: 12px;\n }\n /* Header row above a code block: language label on the left, copy button on\n the right (the surface bar that used to live on .code-lang). */\n .code-head {\n display: flex; align-items: center; justify-content: space-between;\n background: var(--surface0);\n }\n .code-lang {\n color: var(--subtext0); font-size: 10px; padding: 3px 10px; letter-spacing: 0.05em;\n }\n .code-head .copy-btn { padding: 3px 10px; }\n .code-block code {\n display: block; padding: 10px 12px; overflow-x: auto;\n color: var(--text); white-space: pre; line-height: 1.55;\n }\n .hl-kw { color: var(--mauve); }\n .hl-str { color: var(--green); }\n .hl-cmt { color: var(--subtext0); font-style: italic; }\n .hl-num { color: var(--blue); }\n .hl-fn { color: var(--yellow); }\n /* Hand-rolled markdown, applied only to assistant bubbles + the recommendation\n panel (both get the .md class from setContent). Block elements lay themselves\n out, so switch off the container's pre-wrap for these. */\n .md { white-space: normal; }\n .md > :first-child { margin-top: 0; }\n .md > :last-child { margin-bottom: 0; }\n .md .md-h { color: var(--mauve); font-weight: 700; line-height: 1.3; margin: 10px 0 4px; }\n .md .md-h1 { font-size: 1.35em; }\n .md .md-h2 { font-size: 1.2em; }\n .md .md-h3 { font-size: 1.08em; }\n .md .md-h4, .md .md-h5, .md .md-h6 { font-size: 1em; }\n .md .md-p { margin: 6px 0; }\n .md .md-list { margin: 6px 0; padding-left: 22px; }\n .md .md-list li { margin: 2px 0; }\n .md .md-task { list-style: none; margin-left: -22px; }\n .md .md-check { color: var(--green); }\n .md .md-quote { border-left: 3px solid var(--surface2); margin: 6px 0; padding: 2px 0 2px 10px; color: var(--subtext1); }\n .md .md-hr { border: none; border-top: 1px solid var(--surface1); margin: 10px 0; }\n .md a { color: var(--blue); text-decoration: underline; }\n .md strong { color: var(--text); font-weight: 700; }\n .md em { font-style: italic; }\n .md del { color: var(--subtext0); }\n .md .md-code {\n background: var(--crust); border: 1px solid var(--surface0);\n border-radius: 4px; padding: 1px 4px; font-size: 0.92em;\n }\n .md .md-table { border-collapse: collapse; margin: 8px 0; font-size: 0.95em; display: block; overflow-x: auto; }\n .md .md-table th, .md .md-table td { border: 1px solid var(--surface1); padding: 4px 8px; text-align: left; }\n .md .md-table th { background: var(--surface0); color: var(--subtext1); }\n #input-bar {\n background: var(--mantle); padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));\n display: flex; gap: 8px; flex-shrink: 0;\n border-top: 1px solid var(--surface0);\n position: relative;\n }\n #cmd-suggestions {\n display: none; position: absolute; bottom: 100%; left: 16px; right: 16px;\n background: var(--mantle); border: 1px solid var(--surface1);\n border-bottom: none; border-radius: 8px 8px 0 0;\n overflow: hidden; z-index: 10;\n }\n .cmd-item {\n display: flex; align-items: baseline; gap: 10px;\n padding: 7px 12px; cursor: pointer; font-size: 12px;\n border-bottom: 1px solid var(--surface0);\n }\n .cmd-item:last-child { border-bottom: none; }\n .cmd-item:hover, .cmd-item.active { background: var(--surface0); }\n .cmd-item .cmd-name { color: var(--blue); font-weight: bold; flex-shrink: 0; }\n .cmd-item .cmd-desc { color: var(--subtext0); }\n #input {\n flex: 1; background: var(--surface0); color: var(--text);\n border: none; border-radius: 6px; padding: 8px 12px;\n font-family: inherit; font-size: 13px; resize: none;\n outline: none; line-height: 1.5; min-height: 36px; max-height: 120px;\n }\n #input::placeholder { color: var(--subtext0); }\n #input:focus { box-shadow: 0 0 0 1px var(--mauve); }\n #send {\n background: var(--blue); color: var(--crust); border: none;\n border-radius: 6px; padding: 8px 16px; font-weight: bold;\n cursor: pointer; font-size: 13px; font-family: inherit;\n white-space: nowrap; align-self: flex-end;\n }\n #send:disabled, #input:disabled { opacity: 0.45; cursor: not-allowed; }\n /* While the agent runs, Send becomes a red Stop; the armed (tap-to-confirm)\n state brightens it and adds a halo, mirroring the prompt card's cancel. */\n #send.stop { background: var(--red); color: var(--crust); }\n #send.stop.armed {\n filter: brightness(1.12);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--red) 45%, transparent);\n }\n #reconnect-overlay {\n display: none; position: fixed; inset: 0;\n background: rgba(30,30,46,0.88); color: var(--subtext1);\n justify-content: center; align-items: center;\n font-size: 13px; z-index: 100; letter-spacing: 0.03em;\n }\n #reconnect-overlay.visible { display: flex; }\n /* Trailing stream indicator: the same braille spinner as the thinking bubble,\n inline at the end of the streaming text (not a green blinking block). */\n .cursor {\n color: var(--mauve); margin-left: 2px;\n font-family: ui-monospace, monospace;\n }\n #status-panel { padding: 6px 12px; border-bottom: 1px solid var(--surface1);\n color: var(--subtext1); white-space: pre-wrap; font-size: 13px; display: none; }\n /* Structured task widget (progress bar + phase badge + elapsed). Replaces the\n plain text lines when the server sends a structured data payload. */\n #status-panel.structured { white-space: normal; display: block; }\n /* Title gets its own line and wraps (clamped to 2) \u2014 never truncated to a stub\n the way the old single-row flex layout squeezed it on narrow/mobile widths. */\n .widget-title { display: -webkit-box; -webkit-box-orient: vertical;\n -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; color: var(--text);\n font-size: 13px; line-height: 1.3; word-break: break-word; }\n .widget-meta { display: flex; align-items: center; gap: 8px; margin-top: 5px; }\n .widget-phase { flex-shrink: 0; background: var(--surface0); color: var(--mauve);\n border-radius: 4px; padding: 1px 7px; font-size: 10px; letter-spacing: 0.03em;\n text-transform: uppercase; }\n .widget-step { flex-shrink: 0; color: var(--subtext0); font-size: 11px;\n font-variant-numeric: tabular-nums; }\n .widget-elapsed { flex-shrink: 0; margin-left: auto; color: var(--subtext0);\n font-size: 11px; font-variant-numeric: tabular-nums; }\n .widget-bar { height: 4px; background: var(--surface0); border-radius: 2px;\n overflow: hidden; margin-top: 6px; }\n .widget-bar-fill { height: 100%; background: var(--mauve); border-radius: 2px;\n transition: width 0.3s ease; }\n /* Current action \u2014 the terminal's \u21B3 worker trailer, one dim ellipsized line. */\n .widget-action { margin-top: 6px; color: var(--subtext0); font-size: 11px;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n font-variant-numeric: tabular-nums; }\n /* Dim per-turn timestamp shown under a committed turn's bubble. */\n .turn-time { font-size: 10px; color: var(--subtext0); opacity: 0.55; padding: 0 4px;\n margin-top: -2px; }\n .turn-time.user { align-self: flex-end; }\n .turn-time.assistant, .turn-time.system { align-self: flex-start; }\n .turn-time.system { align-self: center; }\n #prompt-card { position: fixed; left: 0; right: 0; bottom: 0; background: var(--mantle);\n border-top: 2px solid var(--mauve); padding: 16px 14px calc(16px + env(safe-area-inset-bottom, 0px));\n display: none; z-index: 50; max-height: 80dvh; overflow-y: auto; }\n #prompt-card .q-label { color: var(--mauve); font-size: 11px; font-weight: 700;\n text-transform: uppercase; letter-spacing: .6px; margin-bottom: 6px; }\n #prompt-card .q { color: var(--text); margin-bottom: 12px; white-space: pre-wrap;\n font-size: 15px; line-height: 1.5; }\n #prompt-card .rec-panel { background: var(--surface0); border-left: 3px solid var(--green);\n border-radius: 6px; padding: 10px 12px; margin-bottom: 12px; }\n #prompt-card .rec-label { color: var(--green); font-size: 11px; font-weight: 700;\n text-transform: uppercase; letter-spacing: .5px; margin-bottom: 4px; }\n #prompt-card .rec-text { color: var(--text); font-size: 15px; line-height: 1.5;\n white-space: pre-wrap; overflow-wrap: anywhere; }\n #prompt-card textarea { width: 100%; background: var(--surface0); color: var(--text);\n border: 1px solid var(--surface2); border-radius: 6px; padding: 10px; font-size: 15px;\n font-family: inherit; line-height: 1.5; resize: vertical; margin-bottom: 4px; }\n #prompt-card .row { display: flex; gap: 8px; margin-top: 12px; align-items: stretch;\n flex-wrap: wrap; }\n /* Recommendation answers can be long sentences, so stack them as a readable list. */\n #prompt-card .row.stacked { flex-direction: column; align-items: stretch; }\n #prompt-card .row.stacked button { flex: none; text-align: left; }\n #prompt-card .row.stacked button.cancel { align-self: center; text-align: center; }\n #prompt-card button { padding: 11px 16px; border-radius: 8px; border: none; cursor: pointer;\n font-family: inherit; font-size: 14px; font-weight: 600; transition: filter .15s ease; }\n #prompt-card button:hover { filter: brightness(1.08); }\n #prompt-card button.primary { background: var(--green); color: var(--crust);\n font-weight: 700; flex: 1; min-width: 160px; }\n #prompt-card button.secondary { background: var(--surface1); color: var(--text);\n flex: 1; min-width: 160px; }\n #prompt-card button.cancel { margin-left: auto; align-self: center; background: transparent;\n color: var(--subtext0); font-size: 12px; font-weight: 500; padding: 8px 10px; }\n #prompt-card button.cancel:hover { color: var(--red); filter: none; }\n #prompt-card button.cancel.armed { background: var(--red); color: var(--crust); font-weight: 700; }\n .toast { position: fixed; top: calc(env(safe-area-inset-top, 0px) + 12px);\n right: calc(env(safe-area-inset-right, 0px) + 12px); max-width: calc(100vw - 24px);\n padding: 8px 12px; border-radius: 6px; overflow-wrap: anywhere; word-break: break-word;\n background: var(--surface1); color: var(--text); z-index: 60; }\n .toast.warning { background: var(--peach); color: var(--crust); }\n .toast.error { background: var(--red); color: var(--crust); }\n #viewer { position: fixed; inset: 24px; background: var(--mantle); border: 1px solid var(--surface2);\n border-radius: 8px; padding: 16px; overflow: auto; white-space: pre-wrap;\n overflow-wrap: anywhere; word-break: break-word; display: none; z-index: 70; }\n #viewer .close { position: absolute; top: 8px; right: 12px; cursor: pointer; color: var(--subtext0); }\n /* Desktop: center the transcript/status/input in a readable column instead of\n hugging the left edge. The scrollbar stays at the true window edge; only the\n content is inset. Mobile (below 960px) is unchanged. */\n @media (min-width: 960px) {\n #chat-log { padding-left: calc((100% - 920px) / 2); padding-right: calc((100% - 920px) / 2); }\n #status-panel { padding-left: calc((100% - 920px) / 2 + 12px); padding-right: calc((100% - 920px) / 2 + 12px); }\n #input-bar { padding-left: calc((100% - 920px) / 2); padding-right: calc((100% - 920px) / 2); }\n #cmd-suggestions { left: calc((100% - 920px) / 2 + 16px); right: calc((100% - 920px) / 2 + 16px); }\n }";
|
package/dist/remote/ui-styles.js
CHANGED
|
@@ -315,18 +315,27 @@ export const STYLES = ` :root {
|
|
|
315
315
|
/* Structured task widget (progress bar + phase badge + elapsed). Replaces the
|
|
316
316
|
plain text lines when the server sends a structured data payload. */
|
|
317
317
|
#status-panel.structured { white-space: normal; display: block; }
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
318
|
+
/* Title gets its own line and wraps (clamped to 2) — never truncated to a stub
|
|
319
|
+
the way the old single-row flex layout squeezed it on narrow/mobile widths. */
|
|
320
|
+
.widget-title { display: -webkit-box; -webkit-box-orient: vertical;
|
|
321
|
+
-webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; color: var(--text);
|
|
322
|
+
font-size: 13px; line-height: 1.3; word-break: break-word; }
|
|
323
|
+
.widget-meta { display: flex; align-items: center; gap: 8px; margin-top: 5px; }
|
|
321
324
|
.widget-phase { flex-shrink: 0; background: var(--surface0); color: var(--mauve);
|
|
322
325
|
border-radius: 4px; padding: 1px 7px; font-size: 10px; letter-spacing: 0.03em;
|
|
323
326
|
text-transform: uppercase; }
|
|
324
|
-
.widget-
|
|
327
|
+
.widget-step { flex-shrink: 0; color: var(--subtext0); font-size: 11px;
|
|
325
328
|
font-variant-numeric: tabular-nums; }
|
|
329
|
+
.widget-elapsed { flex-shrink: 0; margin-left: auto; color: var(--subtext0);
|
|
330
|
+
font-size: 11px; font-variant-numeric: tabular-nums; }
|
|
326
331
|
.widget-bar { height: 4px; background: var(--surface0); border-radius: 2px;
|
|
327
332
|
overflow: hidden; margin-top: 6px; }
|
|
328
333
|
.widget-bar-fill { height: 100%; background: var(--mauve); border-radius: 2px;
|
|
329
334
|
transition: width 0.3s ease; }
|
|
335
|
+
/* Current action — the terminal's ↳ worker trailer, one dim ellipsized line. */
|
|
336
|
+
.widget-action { margin-top: 6px; color: var(--subtext0); font-size: 11px;
|
|
337
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
338
|
+
font-variant-numeric: tabular-nums; }
|
|
330
339
|
/* Dim per-turn timestamp shown under a committed turn's bubble. */
|
|
331
340
|
.turn-time { font-size: 10px; color: var(--subtext0); opacity: 0.55; padding: 0 4px;
|
|
332
341
|
margin-top: -2px; }
|
|
@@ -20,6 +20,9 @@ export type SpawnFn = (command: string, args: ReadonlyArray<string>, options: {
|
|
|
20
20
|
cwd: string;
|
|
21
21
|
shell: boolean;
|
|
22
22
|
stdio: ['ignore' | 'pipe', 'pipe', 'pipe'];
|
|
23
|
+
/** Set only when the invocation needs env overrides (e.g. GIT_INDEX_FILE);
|
|
24
|
+
* absent → the child inherits this process's environment as before. */
|
|
25
|
+
env?: NodeJS.ProcessEnv;
|
|
23
26
|
}) => ProcLike;
|
|
24
27
|
export interface ChildResult {
|
|
25
28
|
stdout: string;
|
|
@@ -113,9 +116,11 @@ export declare function runChild(spawn: SpawnFn, invocation: {
|
|
|
113
116
|
command: string;
|
|
114
117
|
args: ReadonlyArray<string>;
|
|
115
118
|
stdin?: string;
|
|
119
|
+
env?: NodeJS.ProcessEnv;
|
|
116
120
|
}, cwd: string, signal: AbortSignal | undefined, opts?: RunChildOptions): Promise<ChildResult>;
|
|
117
121
|
export declare function runChildDefault(invocation: {
|
|
118
122
|
command: string;
|
|
119
123
|
args: ReadonlyArray<string>;
|
|
124
|
+
env?: NodeJS.ProcessEnv;
|
|
120
125
|
}, cwd: string, signal: AbortSignal | undefined, opts?: RunChildOptions, spawnFn?: SpawnFn): Promise<ChildResult>;
|
|
121
126
|
export declare function summarizeToolArgs(toolName: string, args: unknown): string;
|
|
@@ -178,7 +178,8 @@ export function runChild(spawn, invocation, cwd, signal, opts) {
|
|
|
178
178
|
const proc = spawn(invocation.command, invocation.args, {
|
|
179
179
|
cwd,
|
|
180
180
|
shell: false,
|
|
181
|
-
stdio: [usesStdin ? 'pipe' : 'ignore', 'pipe', 'pipe']
|
|
181
|
+
stdio: [usesStdin ? 'pipe' : 'ignore', 'pipe', 'pipe'],
|
|
182
|
+
...(invocation.env ? { env: invocation.env } : {})
|
|
182
183
|
});
|
|
183
184
|
if (usesStdin) {
|
|
184
185
|
// pi reads the prompt from stdin and waits for EOF, so write then end.
|
|
@@ -29,6 +29,21 @@ export declare function git(cwd: string, args: string[], signal: AbortSignal | u
|
|
|
29
29
|
exitCode: number;
|
|
30
30
|
aborted: boolean;
|
|
31
31
|
}>;
|
|
32
|
+
/**
|
|
33
|
+
* Paths with unmerged index entries (an in-progress merge conflict), deduped.
|
|
34
|
+
* Empty outside a git repo or on any git error — this is a GUARD input, and a
|
|
35
|
+
* guard that cannot conclude must not block.
|
|
36
|
+
*
|
|
37
|
+
* Why it exists (mx5 run 6): a stale `git stash pop` mid-task left two paths UU;
|
|
38
|
+
* every later commit was doomed, three verify passes ran against a conflicted
|
|
39
|
+
* tree, and — worse — a blind `git add -A` on that index would have silently
|
|
40
|
+
* "resolved" the conflict with whatever happened to be on disk.
|
|
41
|
+
*/
|
|
42
|
+
export declare function gitUnmergedPaths(cwd: string, signal?: AbortSignal, spawnFn?: SpawnFn): Promise<string[]>;
|
|
43
|
+
/** Sha of `refs/stash`, or null when there is no stash (or not a git repo). Used
|
|
44
|
+
* to detect a stash created (or consumed) during a task and left behind — the
|
|
45
|
+
* exact landmine that detonated mx5 run 6 two days after it was pushed. */
|
|
46
|
+
export declare function gitStashRef(cwd: string, signal?: AbortSignal, spawnFn?: SpawnFn): Promise<string | null>;
|
|
32
47
|
/**
|
|
33
48
|
* Stage everything (`git add -A`) and commit it with `message`. Honors
|
|
34
49
|
* .gitignore via git itself. Never throws — failures surface as
|
package/dist/task/auto-commit.js
CHANGED
|
@@ -31,6 +31,39 @@ export async function git(cwd, args, signal, spawnFn) {
|
|
|
31
31
|
const r = await runChildDefault({ command: 'git', args }, cwd, signal, { mode: 'text' }, spawnFn);
|
|
32
32
|
return { stdout: r.stdout, stderr: r.stderr, exitCode: r.exitCode, aborted: r.aborted };
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Paths with unmerged index entries (an in-progress merge conflict), deduped.
|
|
36
|
+
* Empty outside a git repo or on any git error — this is a GUARD input, and a
|
|
37
|
+
* guard that cannot conclude must not block.
|
|
38
|
+
*
|
|
39
|
+
* Why it exists (mx5 run 6): a stale `git stash pop` mid-task left two paths UU;
|
|
40
|
+
* every later commit was doomed, three verify passes ran against a conflicted
|
|
41
|
+
* tree, and — worse — a blind `git add -A` on that index would have silently
|
|
42
|
+
* "resolved" the conflict with whatever happened to be on disk.
|
|
43
|
+
*/
|
|
44
|
+
export async function gitUnmergedPaths(cwd, signal, spawnFn) {
|
|
45
|
+
const r = await git(cwd, ['ls-files', '-u'], signal, spawnFn);
|
|
46
|
+
if (r.exitCode !== 0)
|
|
47
|
+
return [];
|
|
48
|
+
const out = [];
|
|
49
|
+
for (const line of r.stdout.split('\n')) {
|
|
50
|
+
// `<mode> <sha> <stage>\t<path>`
|
|
51
|
+
const tab = line.indexOf('\t');
|
|
52
|
+
if (tab === -1)
|
|
53
|
+
continue;
|
|
54
|
+
const p = line.slice(tab + 1).trim();
|
|
55
|
+
if (p.length > 0 && !out.includes(p))
|
|
56
|
+
out.push(p);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
/** Sha of `refs/stash`, or null when there is no stash (or not a git repo). Used
|
|
61
|
+
* to detect a stash created (or consumed) during a task and left behind — the
|
|
62
|
+
* exact landmine that detonated mx5 run 6 two days after it was pushed. */
|
|
63
|
+
export async function gitStashRef(cwd, signal, spawnFn) {
|
|
64
|
+
const r = await git(cwd, ['rev-parse', '-q', '--verify', 'refs/stash'], signal, spawnFn);
|
|
65
|
+
return r.exitCode === 0 ? r.stdout.trim() : null;
|
|
66
|
+
}
|
|
34
67
|
/**
|
|
35
68
|
* Stage everything (`git add -A`) and commit it with `message`. Honors
|
|
36
69
|
* .gitignore via git itself. Never throws — failures surface as
|
|
@@ -44,21 +77,32 @@ export async function gitCommitAll(cwd, message, signal, spawnFn) {
|
|
|
44
77
|
if (inside.exitCode !== 0 || inside.stdout.trim() !== 'true') {
|
|
45
78
|
return { committed: false, reason: 'not a git repository' };
|
|
46
79
|
}
|
|
47
|
-
// 2.
|
|
80
|
+
// 2. REFUSE on an unmerged index: `git add -A` would silently "resolve" the
|
|
81
|
+
// conflict by staging whatever is on disk, and the commit that followed
|
|
82
|
+
// would bless a half-merged tree. Surface it instead — this state needs a
|
|
83
|
+
// human (or the loop's own loud stop), not a snapshot.
|
|
84
|
+
const unmerged = await gitUnmergedPaths(cwd, signal, spawnFn);
|
|
85
|
+
if (unmerged.length > 0) {
|
|
86
|
+
return {
|
|
87
|
+
committed: false,
|
|
88
|
+
reason: `git commit blocked: unresolved merge conflict (${unmerged.slice(0, 3).join(', ')}${unmerged.length > 3 ? `, +${unmerged.length - 3} more` : ''})`
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// 3. Stage all working-tree changes (new, modified, deleted).
|
|
48
92
|
const add = await git(cwd, ['add', '-A'], signal, spawnFn);
|
|
49
93
|
if (add.aborted)
|
|
50
94
|
return { committed: false, reason: 'cancelled' };
|
|
51
95
|
if (add.exitCode !== 0) {
|
|
52
96
|
return { committed: false, reason: `git add failed: ${firstLine(add.stderr)}` };
|
|
53
97
|
}
|
|
54
|
-
//
|
|
98
|
+
// 4. Anything staged? `git diff --cached --quiet` exits 0 when the index
|
|
55
99
|
// matches HEAD (nothing to commit), 1 when there are staged changes.
|
|
56
100
|
const diff = await git(cwd, ['diff', '--cached', '--quiet'], signal, spawnFn);
|
|
57
101
|
if (diff.aborted)
|
|
58
102
|
return { committed: false, reason: 'cancelled' };
|
|
59
103
|
if (diff.exitCode === 0)
|
|
60
104
|
return { committed: false, reason: 'nothing to commit' };
|
|
61
|
-
//
|
|
105
|
+
// 5. Commit. A failure here is usually missing user.name/user.email config —
|
|
62
106
|
// retry once with a self-supplied identity rather than losing the snapshot
|
|
63
107
|
// (and with it enforce + every differential guard) for the whole run.
|
|
64
108
|
const commit = await git(cwd, ['commit', '-m', message], signal, spawnFn);
|
|
@@ -8,6 +8,30 @@ import { type GateDeps } from './task-gates.js';
|
|
|
8
8
|
*/
|
|
9
9
|
export interface AutoDeps extends GateDeps {
|
|
10
10
|
runChild: (name: string, tools: string, prompt: string) => Promise<string>;
|
|
11
|
+
/**
|
|
12
|
+
* Paths with unmerged index entries (an in-progress merge conflict). The loop
|
|
13
|
+
* refuses to START a task on a conflicted tree — mx5 run 6 ran a full impl turn
|
|
14
|
+
* plus three verifies against one, with every commit doomed from the outset.
|
|
15
|
+
* Absent (tests) → treated as clean.
|
|
16
|
+
*/
|
|
17
|
+
unmergedPaths?: (cwd: string) => Promise<string[]>;
|
|
18
|
+
/**
|
|
19
|
+
* Sha of refs/stash or null. Compared around each task so a stash pushed (or
|
|
20
|
+
* consumed) during the task and left behind is called out — an orphan stash is
|
|
21
|
+
* exactly the landmine that detonated as an unresolvable conflict in run 6.
|
|
22
|
+
* Absent (tests) → the check is skipped.
|
|
23
|
+
*/
|
|
24
|
+
stashRef?: (cwd: string) => Promise<string | null>;
|
|
25
|
+
/**
|
|
26
|
+
* Whole-repo FINAL integration gate, run once when every task is checked off
|
|
27
|
+
* and BEFORE the run is declared complete (see final-gate.ts): the project's
|
|
28
|
+
* own static checks plus its own test/build commands, unaided. Absent (tests /
|
|
29
|
+
* gate off) → the run completes as before.
|
|
30
|
+
*/
|
|
31
|
+
finalGate?: (cwd: string) => Promise<{
|
|
32
|
+
ok: boolean;
|
|
33
|
+
reason: string;
|
|
34
|
+
}>;
|
|
11
35
|
}
|
|
12
36
|
/**
|
|
13
37
|
* Expand any @file references in the feature text by appending each referenced
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import * as fsp from 'node:fs/promises';
|
|
9
9
|
import * as path from 'node:path';
|
|
10
|
-
import { gateRunTask } from './orchestrator.js';
|
|
10
|
+
import { gateRunTask, markResumable } from './orchestrator.js';
|
|
11
11
|
import { parseClarifyList, parseAutoAnswer, autoAnswerHasTag, deriveTitle } from './parsers.js';
|
|
12
12
|
import { renderInlineMarkdown, stripInlineMarkdown } from './inline-markdown.js';
|
|
13
13
|
import { AUTO_CLARIFY_PROMPT, AUTO_DECOMPOSE_PROMPT, DECOMPOSE_COVERAGE_PROMPT } from './auto-prompts.js';
|
|
@@ -25,6 +25,9 @@ import { startAutoLoader } from './widget.js';
|
|
|
25
25
|
import { getParentContextWindow, resolveContextUsage } from './context-usage.js';
|
|
26
26
|
import { buildGateDeps } from './gate-deps.js';
|
|
27
27
|
import { runGatesForTask } from './task-gates.js';
|
|
28
|
+
import { gitUnmergedPaths, gitStashRef } from './auto-commit.js';
|
|
29
|
+
import { runFinalIntegrationGate } from './final-gate.js';
|
|
30
|
+
import { getConfig } from '../config/config.js';
|
|
28
31
|
// Hard ceiling on clarify questions per feature. The loop is open-ended (it stops
|
|
29
32
|
// when the model emits NONE), but a model that never says NONE would otherwise
|
|
30
33
|
// barrage the user — the real mx5 run asked 10, several of them redundant.
|
|
@@ -537,7 +540,15 @@ function defaultDeps(ctx, cwd, signal, title) {
|
|
|
537
540
|
stopLoader();
|
|
538
541
|
}
|
|
539
542
|
},
|
|
540
|
-
...buildGateDeps({ signal, parentContextWindow, runTask: gateRunTask })
|
|
543
|
+
...buildGateDeps({ signal, parentContextWindow, runTask: gateRunTask }),
|
|
544
|
+
// Loop-level repo integrity + run-end gate glue (see AutoDeps docs).
|
|
545
|
+
unmergedPaths: cwd2 => gitUnmergedPaths(cwd2, signal),
|
|
546
|
+
stashRef: cwd2 => gitStashRef(cwd2, signal),
|
|
547
|
+
// The final integration gate follows the `verify work` switch: it is the
|
|
548
|
+
// run-level half of the same verification story.
|
|
549
|
+
finalGate: cwd2 => getConfig().verifyWork ?
|
|
550
|
+
Promise.resolve(runFinalIntegrationGate(cwd2))
|
|
551
|
+
: Promise.resolve({ ok: true, reason: 'disabled' })
|
|
541
552
|
};
|
|
542
553
|
}
|
|
543
554
|
// ─── Loop ────────────────────────────────────────────────────────────────────
|
|
@@ -578,10 +589,58 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
578
589
|
const entries = parseTaskList(body);
|
|
579
590
|
const next = entries.find(e => !e.done);
|
|
580
591
|
if (!next) {
|
|
592
|
+
// FINAL INTEGRATION GATE: every task passed its own per-slice gates,
|
|
593
|
+
// but per-slice green has shipped a dead app twice (mx5 runs 3 & 5:
|
|
594
|
+
// statics clean, every protected route 500ing). Run the project's
|
|
595
|
+
// OWN whole-repo commands once, unaided, before declaring the run
|
|
596
|
+
// complete. On a FAIL the user decides: accept (complete anyway) or
|
|
597
|
+
// leave the run failed — a resume re-enters this branch and re-runs
|
|
598
|
+
// the gate, so fixing and resuming converges.
|
|
599
|
+
if (deps.finalGate) {
|
|
600
|
+
active.ui.notify(`${id}: running final integration gate…`, 'info');
|
|
601
|
+
const fin = await deps.finalGate(cwd);
|
|
602
|
+
if (!fin.ok) {
|
|
603
|
+
const question = `Final integration gate FAILED for ${id}.\n\n${fin.reason}\n\n`
|
|
604
|
+
+ 'All tasks are checked off — this is the whole-repo check '
|
|
605
|
+
+ '(the project’s own test/build/static commands, run unaided).';
|
|
606
|
+
const answer = await new SessionUI(active).ask({
|
|
607
|
+
localTitle: 'Final integration gate failed — how should pi proceed?',
|
|
608
|
+
displayQuestion: question,
|
|
609
|
+
question,
|
|
610
|
+
recommended: 'Leave failed — I will fix and /task-auto-resume',
|
|
611
|
+
recommended2: 'Accept — complete the run anyway',
|
|
612
|
+
allowSkip: false,
|
|
613
|
+
options: [
|
|
614
|
+
{
|
|
615
|
+
label: 'Leave failed — I will fix and /task-auto-resume',
|
|
616
|
+
value: 'fail'
|
|
617
|
+
},
|
|
618
|
+
{ label: 'Accept — complete the run anyway', value: 'accept' }
|
|
619
|
+
]
|
|
620
|
+
});
|
|
621
|
+
if (answer === undefined || !/^accept\b/i.test(answer.trim())) {
|
|
622
|
+
await updateTaskFrontMatter(cwd, id, { state: 'failed' });
|
|
623
|
+
announceDone(active, `${id} finished all tasks but FAILED the final integration gate — ${fin.reason.slice(0, 200)} — fix and /task-auto-resume (the gate re-runs).`, 'error');
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
active.ui.notify(`${id}: final integration gate FAIL accepted by user — completing.`, 'warning');
|
|
627
|
+
}
|
|
628
|
+
}
|
|
581
629
|
await updateTaskFrontMatter(cwd, id, { state: 'completed' });
|
|
582
630
|
announceDone(active, `${id} complete — all ${entries.length} tasks done.`, 'info');
|
|
583
631
|
return;
|
|
584
632
|
}
|
|
633
|
+
// REFUSE to start on a conflicted tree: an unmerged index dooms every
|
|
634
|
+
// commit ahead and a `git add -A` would silently mis-resolve it. mx5
|
|
635
|
+
// run 6 burned a full impl turn + three verify passes exactly here.
|
|
636
|
+
const unmerged = deps.unmergedPaths ? await deps.unmergedPaths(cwd) : [];
|
|
637
|
+
if (unmerged.length > 0) {
|
|
638
|
+
await updateTaskFrontMatter(cwd, id, { state: 'failed' });
|
|
639
|
+
announceDone(active, `${id} stopped before "${next.title}" — the repository has unresolved merge conflicts `
|
|
640
|
+
+ `(${unmerged.slice(0, 3).join(', ')}${unmerged.length > 3 ? `, +${unmerged.length - 3} more` : ''}). `
|
|
641
|
+
+ 'Resolve them (git status), then /task-auto-resume.', 'error');
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
585
644
|
active.ui.notify(`${id}: task ${next.index + 1}/${entries.length} — ${next.title}`, 'info');
|
|
586
645
|
// If this entry already has a stamped inner id, it was started in a
|
|
587
646
|
// previous (interrupted) run — resume it from its saved phase rather
|
|
@@ -612,6 +671,10 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
612
671
|
if (checkpoint.committed) {
|
|
613
672
|
active.ui.notify(`${id}: checkpointed uncommitted work before "${next.title}".`, 'info');
|
|
614
673
|
}
|
|
674
|
+
// Stash ref before the task: compared after the gates so a stash pushed
|
|
675
|
+
// during the task (impl model or any child) and left behind is called
|
|
676
|
+
// out instead of silently waiting to detonate in a later task.
|
|
677
|
+
const stashBefore = deps.stashRef ? await deps.stashRef(cwd) : undefined;
|
|
615
678
|
const res = await deps.runTask(active, cwd, next.title, {
|
|
616
679
|
resumeId,
|
|
617
680
|
// Fence this step against re-expanding the whole referenced spec:
|
|
@@ -634,10 +697,15 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
634
697
|
// this task's spec to finish it. (A plain ESC that the user
|
|
635
698
|
// follows with steering text never reaches here — that loops on
|
|
636
699
|
// the same task inside runSingleTask until a turn completes.)
|
|
700
|
+
await markResumable(cwd, res.taskId);
|
|
637
701
|
announceDone(active, `${id} paused at "${next.title}" — resume with /task-auto-resume.`, 'warning');
|
|
638
702
|
return;
|
|
639
703
|
}
|
|
640
704
|
if (!res.ok) {
|
|
705
|
+
// Demote the INNER task file too: it reads `completed` from
|
|
706
|
+
// spec-handoff, and leaving it that way is how a failed run's task
|
|
707
|
+
// file claimed success in the run 6 audit.
|
|
708
|
+
await markResumable(cwd, res.taskId);
|
|
641
709
|
await updateTaskFrontMatter(cwd, id, { state: 'failed' });
|
|
642
710
|
// res.reason is set when the implementation turn itself died
|
|
643
711
|
// (e.g. a context-overflow 400) — surface it so the real cause
|
|
@@ -668,6 +736,7 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
668
736
|
});
|
|
669
737
|
active = gate.ctx;
|
|
670
738
|
if (gate.kind === 'paused') {
|
|
739
|
+
await markResumable(cwd, res.taskId);
|
|
671
740
|
await updateTaskFrontMatter(cwd, id, { state: 'failed' });
|
|
672
741
|
announceDone(active, `${id} paused at "${next.title}" — verification failed and you dismissed the choice; resume with /task-auto-resume.`, 'warning');
|
|
673
742
|
return;
|
|
@@ -677,16 +746,26 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
677
746
|
return;
|
|
678
747
|
}
|
|
679
748
|
if (gate.kind === 'interrupted') {
|
|
749
|
+
await markResumable(cwd, res.taskId);
|
|
680
750
|
announceDone(active, `${id} paused at "${next.title}" — resume with /task-auto-resume.`, 'warning');
|
|
681
751
|
return;
|
|
682
752
|
}
|
|
683
753
|
if (gate.kind === 'failed') {
|
|
754
|
+
await markResumable(cwd, res.taskId);
|
|
684
755
|
await updateTaskFrontMatter(cwd, id, { state: 'failed' });
|
|
685
756
|
const why = gate.reason ? ` — ${gate.reason.slice(0, 160)}` : '';
|
|
686
757
|
announceDone(active, `${id} stopped at "${next.title}"${why} — fix and run /task-auto-resume.`, 'error');
|
|
687
758
|
return;
|
|
688
759
|
}
|
|
689
|
-
// gate.kind === 'done' → fall through to the next task
|
|
760
|
+
// gate.kind === 'done' → fall through to the next task, after checking
|
|
761
|
+
// no landmine stash was left behind by anything that ran in between.
|
|
762
|
+
if (deps.stashRef && stashBefore !== undefined) {
|
|
763
|
+
const stashAfter = await deps.stashRef(cwd);
|
|
764
|
+
if (stashAfter !== stashBefore) {
|
|
765
|
+
active.ui.notify(`${id}: the git stash stack changed during "${next.title}" and was left that way — `
|
|
766
|
+
+ 'inspect `git stash list`; an orphan stash later pops as an unresolvable conflict.', 'warning');
|
|
767
|
+
}
|
|
768
|
+
}
|
|
690
769
|
}
|
|
691
770
|
}
|
|
692
771
|
catch (err) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type HealthCommand } from './repo-health-check.js';
|
|
2
|
+
export interface FinalGateOutcome {
|
|
3
|
+
/** true → statics and every runnable integration command passed (or nothing to run). */
|
|
4
|
+
ok: boolean;
|
|
5
|
+
/** On a fail: the exact command, its exit code, and the tail of its output. */
|
|
6
|
+
reason: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The project's OWN whole-repo integration commands (test, then build — test
|
|
10
|
+
* first because it is the richer signal and the more common script). First
|
|
11
|
+
* manifest that exists wins, mirroring discoverHealthCommands. Empty means
|
|
12
|
+
* "nothing to run" — the static half may still gate.
|
|
13
|
+
*/
|
|
14
|
+
export declare function discoverIntegrationCommands(cwd: string): {
|
|
15
|
+
ecosystem: string | null;
|
|
16
|
+
cmds: HealthCommand[];
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Run the final gate: static analysis first, then the discovered integration
|
|
20
|
+
* commands, whole-repo, verbatim, unaided. Deterministic and synchronous under
|
|
21
|
+
* the hood (callers wrap in Promise.resolve). First real failure wins.
|
|
22
|
+
*/
|
|
23
|
+
export declare function runFinalIntegrationGate(cwd: string, timeoutMs?: number): FinalGateOutcome;
|