create-walle 0.9.32 → 0.9.33
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/package.json +1 -1
- package/template/claude-task-manager/docs/postgres-storage-abstraction.html +732 -0
- package/template/claude-task-manager/lib/headless-term-service.js +29 -1
- package/template/claude-task-manager/lib/state-sync/emit-clamp.js +32 -0
- package/template/claude-task-manager/lib/state-sync/frame-emitter.js +1 -1
- package/template/claude-task-manager/lib/walle-ctm-history.js +115 -6
- package/template/claude-task-manager/public/css/walle-session.css +57 -1
- package/template/claude-task-manager/public/index.html +66 -8
- package/template/claude-task-manager/public/js/message-renderer.js +87 -0
- package/template/claude-task-manager/public/js/state-sync-client.js +32 -0
- package/template/claude-task-manager/public/js/walle-session.js +324 -90
- package/template/claude-task-manager/server.js +122 -7
- package/template/claude-task-manager/workers/state-detectors/codex.js +25 -6
- package/template/claude-task-manager/workers/state-detectors/index.js +1 -0
- package/template/package.json +1 -1
- package/template/wall-e/chat.js +7 -75
- package/template/wall-e/coding/coding-run-controller.js +118 -195
- package/template/wall-e/coding/compaction-service.js +91 -10
- package/template/wall-e/coding/no-progress-guard.js +4 -1
- package/template/wall-e/coding/output-boundary.js +94 -0
- package/template/wall-e/coding/permission-service.js +15 -0
- package/template/wall-e/coding/session-workspaces.js +70 -0
- package/template/wall-e/coding/tool-execution-controller.js +54 -3
- package/template/wall-e/coding-orchestrator.js +40 -299
- package/template/wall-e/coding-prompts.js +62 -8
- package/template/wall-e/context/token-counter.js +35 -15
- package/template/wall-e/tools/local-tools.js +45 -17
- package/template/wall-e/tools/permission-checker.js +5 -0
|
@@ -710,6 +710,24 @@ async function handleMessage(msg) {
|
|
|
710
710
|
break;
|
|
711
711
|
}
|
|
712
712
|
|
|
713
|
+
// Option B: bottom-anchor clamp. The main thread sets this to the held primary SHRINK target while a
|
|
714
|
+
// rows-only resize is deferred mid-stream, so extractGrid emits only the bottom N viewport rows (the
|
|
715
|
+
// agent's composer stays inside the frame instead of CUP-clamping blank onto a shorter client). rows<=0
|
|
716
|
+
// clears it. The emitted grid SHAPE changes, so force a clean keyframe on any change (diffing a
|
|
717
|
+
// bottom-anchored grid against a top-anchored acked grid would smear the transition).
|
|
718
|
+
case 'set-emit-clamp': {
|
|
719
|
+
const entry = terminals.get(msg.sessionId);
|
|
720
|
+
if (!entry) break;
|
|
721
|
+
const next = Math.max(0, Number(msg.rows) || 0);
|
|
722
|
+
if (Number(entry.emitClampRows || 0) === next) break; // no change → no keyframe churn
|
|
723
|
+
entry.emitClampRows = next;
|
|
724
|
+
if (_stateSyncEmitter) {
|
|
725
|
+
if (_stateSyncFramesSuppressed(msg.sessionId)) _stateSyncMarkHiddenDirty(msg.sessionId);
|
|
726
|
+
else _stateSyncEmitter.onKeyframeReq(msg.sessionId, {});
|
|
727
|
+
}
|
|
728
|
+
break;
|
|
729
|
+
}
|
|
730
|
+
|
|
713
731
|
// Option B: client confirmed it applied frame `seq` (no-op when state-sync is off).
|
|
714
732
|
case 'ack': {
|
|
715
733
|
if (_stateSyncEmitter) _stateSyncEmitter.onAck(msg.sessionId, Number(msg.seq || 0));
|
|
@@ -946,8 +964,18 @@ function extractGrid(sessionId, opts) {
|
|
|
946
964
|
if (!length || !viewportRows) return { cols: Number(term.cols || 0), rows: 0, rowAnsi: [], baseRow: 0 };
|
|
947
965
|
const scrollbackRows = Math.max(0, Number(opts && opts.scrollbackRows) || 0);
|
|
948
966
|
const baseY = Math.max(0, Number(buf.baseY || 0));
|
|
949
|
-
|
|
967
|
+
// Bottom-anchor clamp: while a primary rows-only SHRINK resize is HELD (StreamResizePolicy flap-hold),
|
|
968
|
+
// the PTY/mirror stays TALLER than the already-shrunk client. A top-aligned full-height frame would push
|
|
969
|
+
// the agent's bottom-pinned composer past the client's last row, where every CUP write clamps onto it and
|
|
970
|
+
// the composer renders blank. Emit only the BOTTOM `emitClampRows` viewport rows so the composer is always
|
|
971
|
+
// inside the frame and the (matching-height) client paints it 1:1. Sacrifices the top (held-delta) viewport
|
|
972
|
+
// rows for the turn; the held resize reconciles them on apply (keyframe). 0 / >= viewportRows ⇒ no clamp.
|
|
973
|
+
// Only narrows the LIVE viewport window (scrollbackRows === 0) — snapshot/scrollback extraction is untouched.
|
|
974
|
+
const clampRows = Math.max(0, Number(entry.emitClampRows || 0));
|
|
975
|
+
const topTrim = (scrollbackRows === 0 && clampRows > 0 && clampRows < viewportRows) ? (viewportRows - clampRows) : 0;
|
|
950
976
|
const end = Math.min(length - 1, baseY + viewportRows - 1);
|
|
977
|
+
let start = Math.max(0, baseY + topTrim - scrollbackRows);
|
|
978
|
+
if (start > end) start = Math.max(0, baseY - scrollbackRows); // defensive: never invert the range
|
|
951
979
|
const rowAnsi = [];
|
|
952
980
|
for (let r = start; r <= end; r++) {
|
|
953
981
|
rowAnsi.push(serializeRow(buf.getLine(r), term.cols));
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Option B state-sync — bottom-anchor emit clamp decision (server side).
|
|
3
|
+
//
|
|
4
|
+
// While a primary rows-only SHRINK resize is HELD mid-stream (StreamResizePolicy flap-hold), the PTY/mirror
|
|
5
|
+
// stays TALLER than the already-shrunk client. A top-aligned full-height frame then positions the agent's
|
|
6
|
+
// bottom-pinned composer at rows past the client's last row, where xterm CUP-clamps every write onto the
|
|
7
|
+
// bottom row and the composer renders BLANK (the "running Claude composer is blank, reflow won't fix" bug).
|
|
8
|
+
// The fix bottom-anchors the worker's emitted grid to the held target height so the composer stays inside
|
|
9
|
+
// the frame; this predicate decides the clamp value from the PTY + held-resize dims.
|
|
10
|
+
//
|
|
11
|
+
// Returns the clampRows (the held target row count) when a bottom-anchor clamp should be engaged, else 0
|
|
12
|
+
// (no clamp / full viewport). Engages ONLY for the rows-only shrink signature:
|
|
13
|
+
// - a resize is held (pend present, positive dims), AND
|
|
14
|
+
// - cols are unchanged (a cols change is a real resize handled promptly via width arbitration — never
|
|
15
|
+
// bottom-anchor it), AND
|
|
16
|
+
// - the held target is strictly SHORTER than the current PTY (a GROW is the client-taller / orphan-rows
|
|
17
|
+
// case, compensated on the client, not here).
|
|
18
|
+
// Fail-safe: any non-finite / non-positive input ⇒ 0 (no clamp).
|
|
19
|
+
function bottomAnchorClampRows(opts) {
|
|
20
|
+
const o = opts || {};
|
|
21
|
+
const ptyCols = Number(o.ptyCols);
|
|
22
|
+
const ptyRows = Number(o.ptyRows);
|
|
23
|
+
const pendCols = Number(o.pendCols);
|
|
24
|
+
const pendRows = Number(o.pendRows);
|
|
25
|
+
if (![ptyCols, ptyRows, pendCols, pendRows].every(Number.isFinite)) return 0;
|
|
26
|
+
if (ptyCols <= 0 || ptyRows <= 0 || pendCols <= 0 || pendRows <= 0) return 0;
|
|
27
|
+
if (pendCols !== ptyCols) return 0; // cols change ⇒ real resize, not the chrome-flap shrink
|
|
28
|
+
if (pendRows >= ptyRows) return 0; // grow / equal ⇒ nothing to bottom-anchor
|
|
29
|
+
return pendRows;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { bottomAnchorClampRows };
|
|
@@ -105,7 +105,7 @@ function createFrameEmitter(deps) {
|
|
|
105
105
|
const cursorMoved = !cursorsEqual(grid.cursor, s.ackedGrid && s.ackedGrid.cursor);
|
|
106
106
|
if (!force && !cursorMoved && gridsEqual(grid, s.ackedGrid)) return false; // nothing changed since last applied frame
|
|
107
107
|
const patch = computeAnsiPatch(force ? EMPTY_GRID : s.ackedGrid, grid);
|
|
108
|
-
if (patch.
|
|
108
|
+
if (!patch.ansi && !force && !cursorMoved) return false;
|
|
109
109
|
s.seq += 1;
|
|
110
110
|
noteEmit(s.fr, t);
|
|
111
111
|
s.dirty = false;
|
|
@@ -398,8 +398,27 @@ function compactWalleToolCallSummary(call = {}) {
|
|
|
398
398
|
return summary ? `${head}: ${summary}` : head;
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
+
function turnActivityCalls(toolCalls = [], sawThinking = false, completed = false) {
|
|
402
|
+
const calls = Array.isArray(toolCalls) ? cloneToolCalls(toolCalls).filter(Boolean) : [];
|
|
403
|
+
if (sawThinking && !calls.some((call) => call && (call.name === 'think' || call.tool === 'think'))) {
|
|
404
|
+
calls.unshift({
|
|
405
|
+
name: 'think',
|
|
406
|
+
tool: 'think',
|
|
407
|
+
summary: completed ? 'Reasoning step completed' : 'Wall-E is thinking',
|
|
408
|
+
status: completed ? 'done' : 'working',
|
|
409
|
+
output: '',
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
if (completed) {
|
|
413
|
+
for (const call of calls) {
|
|
414
|
+
if (call && (!call.status || call.status === 'working')) call.status = 'done';
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return calls;
|
|
418
|
+
}
|
|
419
|
+
|
|
401
420
|
function openTurnActivityMessage(toolCalls = [], sawThinking = false, timestamp = '') {
|
|
402
|
-
const calls =
|
|
421
|
+
const calls = turnActivityCalls(toolCalls, sawThinking, false);
|
|
403
422
|
if (!sawThinking && calls.length === 0) return null;
|
|
404
423
|
const lines = ['Wall-E is working on this prompt.'];
|
|
405
424
|
const visibleCalls = calls.slice(-6);
|
|
@@ -413,6 +432,12 @@ function openTurnActivityMessage(toolCalls = [], sawThinking = false, timestamp
|
|
|
413
432
|
role: 'system',
|
|
414
433
|
content: lines.join('\n'),
|
|
415
434
|
timestamp,
|
|
435
|
+
// Stamp a uuid on this synthesized live-activity row (it has no backing jsonl
|
|
436
|
+
// record) so EVERY displayed message carries a uuid. Without it, the client's
|
|
437
|
+
// all-or-nothing uuid gate used to disable the branch pager for the whole session
|
|
438
|
+
// whenever an open turn was present. It threads to its linear predecessor on the
|
|
439
|
+
// client (no parentUuid), and is replaced on the next durable reload.
|
|
440
|
+
uuid: `walle-open-turn-${timestamp || 'live'}`,
|
|
416
441
|
toolCalls: calls.length
|
|
417
442
|
? cloneToolCalls(calls)
|
|
418
443
|
: [{ name: 'think', tool: 'think', summary: 'Wall-E is thinking', status: 'working' }],
|
|
@@ -422,6 +447,27 @@ function openTurnActivityMessage(toolCalls = [], sawThinking = false, timestamp
|
|
|
422
447
|
};
|
|
423
448
|
}
|
|
424
449
|
|
|
450
|
+
function completedTurnActivityMessage(toolCalls = [], sawThinking = false, timestamp = '', key = '') {
|
|
451
|
+
const calls = turnActivityCalls(toolCalls, sawThinking, true);
|
|
452
|
+
if (!sawThinking && calls.length === 0) return null;
|
|
453
|
+
const lines = ['Wall-E activity log for this prompt.'];
|
|
454
|
+
const visibleCalls = calls.slice(-8);
|
|
455
|
+
if (visibleCalls.length) {
|
|
456
|
+
for (const call of visibleCalls) lines.push(`- ${compactWalleToolCallSummary(call)}`);
|
|
457
|
+
if (calls.length > visibleCalls.length) lines.push(`- +${calls.length - visibleCalls.length} earlier activities`);
|
|
458
|
+
}
|
|
459
|
+
return {
|
|
460
|
+
role: 'system',
|
|
461
|
+
content: lines.join('\n'),
|
|
462
|
+
timestamp,
|
|
463
|
+
uuid: `walle-completed-turn-${key || timestamp || 'activity'}`,
|
|
464
|
+
toolCalls: calls,
|
|
465
|
+
liveActivity: true,
|
|
466
|
+
agentLabel: 'Wall-E',
|
|
467
|
+
metadata: { source: 'walle-completed-turn-activity', completed: true },
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
425
471
|
function terminalBoundaryMessageFromEntry(entry) {
|
|
426
472
|
if (!entry || typeof entry !== 'object') return null;
|
|
427
473
|
const type = String(entry.type || '').trim();
|
|
@@ -597,6 +643,11 @@ function readWalleCtmHistory(filePath) {
|
|
|
597
643
|
let openTurnHasUser = false;
|
|
598
644
|
let pendingAssistantText = '';
|
|
599
645
|
let pendingAssistantTimestamp = '';
|
|
646
|
+
// A coding_summary part is written just BEFORE the turn's final assistant entry
|
|
647
|
+
// (the event fires right before `done`), so buffer it and stamp it onto that
|
|
648
|
+
// assistant message when it's pushed. Also attached to the last assistant at EOF
|
|
649
|
+
// for the rare case the part trails the assistant entry.
|
|
650
|
+
let pendingCodingSummary = null;
|
|
600
651
|
// Durable approval cards: a permission_request part becomes a card message; a
|
|
601
652
|
// later permission_resolved part marks that same card resolved (so a reload
|
|
602
653
|
// shows an answered request as resolved, and a still-pending one as actionable).
|
|
@@ -607,14 +658,27 @@ function readWalleCtmHistory(filePath) {
|
|
|
607
658
|
const parentByUuid = Object.create(null);
|
|
608
659
|
let pendingAssistantUuid = '';
|
|
609
660
|
let pendingAssistantParent = null;
|
|
661
|
+
let syntheticActivityOrdinal = 0;
|
|
662
|
+
const pushCompletedTurnActivity = (toolCalls, sawThinking, timestamp, key) => {
|
|
663
|
+
const activity = completedTurnActivityMessage(
|
|
664
|
+
toolCalls,
|
|
665
|
+
sawThinking,
|
|
666
|
+
timestamp,
|
|
667
|
+
`${key || 'turn'}-${syntheticActivityOrdinal++}`
|
|
668
|
+
);
|
|
669
|
+
if (!activity) return false;
|
|
670
|
+
messages.push(activity);
|
|
671
|
+
return true;
|
|
672
|
+
};
|
|
610
673
|
const flushPendingAssistantText = (options = {}) => {
|
|
611
674
|
const content = normalizeWalleDisplayContent(pendingAssistantText);
|
|
612
675
|
if (!content) { pendingAssistantUuid = ''; pendingAssistantParent = null; return false; }
|
|
613
|
-
|
|
676
|
+
const assistantToolCalls = cloneToolCalls(pendingToolCalls);
|
|
677
|
+
const assistantMessage = {
|
|
614
678
|
role: 'assistant',
|
|
615
679
|
content,
|
|
616
680
|
timestamp: pendingAssistantTimestamp || openTurnActivityTimestamp || 0,
|
|
617
|
-
toolCalls:
|
|
681
|
+
toolCalls: assistantToolCalls,
|
|
618
682
|
uuid: pendingAssistantUuid || undefined,
|
|
619
683
|
parentUuid: pendingAssistantParent != null ? pendingAssistantParent : undefined,
|
|
620
684
|
metadata: {
|
|
@@ -622,7 +686,16 @@ function readWalleCtmHistory(filePath) {
|
|
|
622
686
|
partial: options.partial !== false,
|
|
623
687
|
boundary: options.boundary || '',
|
|
624
688
|
},
|
|
625
|
-
}
|
|
689
|
+
};
|
|
690
|
+
if (pushCompletedTurnActivity(
|
|
691
|
+
assistantToolCalls,
|
|
692
|
+
openTurnSawThinking,
|
|
693
|
+
assistantMessage.timestamp,
|
|
694
|
+
pendingAssistantUuid || assistantMessage.timestamp || 'text'
|
|
695
|
+
)) {
|
|
696
|
+
assistantMessage.metadata.activityLogged = true;
|
|
697
|
+
}
|
|
698
|
+
messages.push(assistantMessage);
|
|
626
699
|
pendingAssistantText = '';
|
|
627
700
|
pendingAssistantTimestamp = '';
|
|
628
701
|
pendingAssistantUuid = '';
|
|
@@ -721,6 +794,14 @@ function readWalleCtmHistory(filePath) {
|
|
|
721
794
|
}
|
|
722
795
|
continue;
|
|
723
796
|
}
|
|
797
|
+
if (entry.type === 'walle_part' && entry.partType === 'coding_summary') {
|
|
798
|
+
// Buffer it — the assistant entry it belongs to is written NEXT. Stored under
|
|
799
|
+
// metadata.codingSummary (not a top-level field) so it survives the durable DB
|
|
800
|
+
// row store, which persists `metadata` but not arbitrary message fields
|
|
801
|
+
// (db._messageRowMeta).
|
|
802
|
+
pendingCodingSummary = entry.data && typeof entry.data === 'object' ? entry.data : {};
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
724
805
|
const textPart = walleTextPartContent(entry);
|
|
725
806
|
if (textPart) {
|
|
726
807
|
if (!pendingAssistantUuid && entry.uuid) {
|
|
@@ -783,7 +864,7 @@ function readWalleCtmHistory(filePath) {
|
|
|
783
864
|
openTurnHasUser = false;
|
|
784
865
|
continue;
|
|
785
866
|
}
|
|
786
|
-
|
|
867
|
+
const assistantMessage = {
|
|
787
868
|
role: 'assistant',
|
|
788
869
|
content: candidateContent,
|
|
789
870
|
model: entry.message?.model || entry.model || entry.modelId || '',
|
|
@@ -792,7 +873,20 @@ function readWalleCtmHistory(filePath) {
|
|
|
792
873
|
uuid: entry.uuid || undefined,
|
|
793
874
|
parentUuid: entry.parentUuid != null ? entry.parentUuid : undefined,
|
|
794
875
|
toolCalls: dedupeToolCallsById(Array.isArray(entry.toolCalls) ? cloneToolCalls(entry.toolCalls) : cloneToolCalls(pendingToolCalls)),
|
|
795
|
-
}
|
|
876
|
+
};
|
|
877
|
+
if (pushCompletedTurnActivity(
|
|
878
|
+
assistantMessage.toolCalls,
|
|
879
|
+
openTurnSawThinking,
|
|
880
|
+
assistantMessage.timestamp,
|
|
881
|
+
entry.uuid || entry.timestamp || 'assistant'
|
|
882
|
+
)) {
|
|
883
|
+
assistantMessage.metadata = Object.assign({}, assistantMessage.metadata, { activityLogged: true });
|
|
884
|
+
}
|
|
885
|
+
if (pendingCodingSummary) {
|
|
886
|
+
assistantMessage.metadata = Object.assign({}, assistantMessage.metadata, { codingSummary: pendingCodingSummary });
|
|
887
|
+
pendingCodingSummary = null;
|
|
888
|
+
}
|
|
889
|
+
messages.push(assistantMessage);
|
|
796
890
|
pendingToolCalls = [];
|
|
797
891
|
openTurnSawThinking = false;
|
|
798
892
|
openTurnActivityTimestamp = '';
|
|
@@ -800,6 +894,17 @@ function readWalleCtmHistory(filePath) {
|
|
|
800
894
|
}
|
|
801
895
|
}
|
|
802
896
|
flushPendingAssistantText({ boundary: 'eof' });
|
|
897
|
+
// Rare: a coding_summary part that trailed the assistant entry (or whose turn
|
|
898
|
+
// ended without a fresh assistant push) — stamp it onto the last assistant.
|
|
899
|
+
if (pendingCodingSummary) {
|
|
900
|
+
for (let k = messages.length - 1; k >= 0; k--) {
|
|
901
|
+
if (messages[k] && messages[k].role === 'assistant') {
|
|
902
|
+
messages[k].metadata = Object.assign({}, messages[k].metadata, { codingSummary: pendingCodingSummary });
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
pendingCodingSummary = null;
|
|
907
|
+
}
|
|
803
908
|
const openActivity = openTurnHasUser
|
|
804
909
|
? openTurnActivityMessage(pendingToolCalls, openTurnSawThinking, openTurnActivityTimestamp)
|
|
805
910
|
: null;
|
|
@@ -1041,6 +1146,10 @@ function cloneMessage(message) {
|
|
|
1041
1146
|
if (message?.metadata) {
|
|
1042
1147
|
try { out.metadata = JSON.parse(JSON.stringify(message.metadata)); } catch { out.metadata = message.metadata; }
|
|
1043
1148
|
}
|
|
1149
|
+
if (message?.codingSummary) {
|
|
1150
|
+
// Whitelist clone — carry the pinned summary-card payload through the merge.
|
|
1151
|
+
try { out.codingSummary = JSON.parse(JSON.stringify(message.codingSummary)); } catch { out.codingSummary = message.codingSummary; }
|
|
1152
|
+
}
|
|
1044
1153
|
if (message?.runtimeDiagnostic) {
|
|
1045
1154
|
try { out.runtimeDiagnostic = JSON.parse(JSON.stringify(message.runtimeDiagnostic)); } catch { out.runtimeDiagnostic = message.runtimeDiagnostic; }
|
|
1046
1155
|
}
|
|
@@ -179,11 +179,13 @@
|
|
|
179
179
|
flex: 0 1 auto;
|
|
180
180
|
min-width: 3ch;
|
|
181
181
|
}
|
|
182
|
+
/* cwd · msgs meta now LEADS the right-aligned control cluster (no title/icon on
|
|
183
|
+
the left — CTM-faithful), so it centre-aligns against the 24px chip/nav/⋯. */
|
|
182
184
|
.walle-session-header .session-meta {
|
|
183
185
|
font-size: 12px;
|
|
184
186
|
color: var(--fg-dim, #565f89);
|
|
185
187
|
display: flex;
|
|
186
|
-
align-items:
|
|
188
|
+
align-items: center;
|
|
187
189
|
gap: 6px;
|
|
188
190
|
min-width: 0;
|
|
189
191
|
flex: 0 1 auto;
|
|
@@ -1187,6 +1189,12 @@ body.walle-composer-resizing {
|
|
|
1187
1189
|
display: grid;
|
|
1188
1190
|
gap: 5px;
|
|
1189
1191
|
margin-top: 7px;
|
|
1192
|
+
/* Append-only progress log (WS1): show every step, but cap the panel height and
|
|
1193
|
+
scroll internally (pinned to the newest step) so a long turn reads like a
|
|
1194
|
+
scrollback without pushing the composer off-screen. */
|
|
1195
|
+
max-height: 340px;
|
|
1196
|
+
overflow-y: auto;
|
|
1197
|
+
overscroll-behavior: contain;
|
|
1190
1198
|
}
|
|
1191
1199
|
.walle-live-activity-item {
|
|
1192
1200
|
display: block;
|
|
@@ -2620,3 +2628,51 @@ html[data-theme="light"] .walle-error-details pre {
|
|
|
2620
2628
|
html[data-theme="light"] .walle-image-viewer {
|
|
2621
2629
|
background: var(--overlay-bg);
|
|
2622
2630
|
}
|
|
2631
|
+
|
|
2632
|
+
/* ── Coding summary card (WS2) ─────────────────────────────────────────────
|
|
2633
|
+
Deterministic, actionable wrap-up pinned atop a completed coding turn. Built
|
|
2634
|
+
by MR.renderCodingSummaryHtml; data from coding-run-controller buildCodingSummary. */
|
|
2635
|
+
.coding-summary-card{
|
|
2636
|
+
border:1px solid var(--border,#3b4261);border-radius:10px;
|
|
2637
|
+
background:var(--bg-card,#24283b);
|
|
2638
|
+
margin:8px 0 12px;overflow:hidden;
|
|
2639
|
+
box-shadow:0 3px 12px rgba(0,0,0,.22);
|
|
2640
|
+
}
|
|
2641
|
+
.coding-summary-card .csum-top{
|
|
2642
|
+
display:flex;align-items:center;gap:9px;padding:9px 13px;
|
|
2643
|
+
border-bottom:1px solid var(--border,#3b4261);
|
|
2644
|
+
background:linear-gradient(180deg,rgba(122,162,247,.07),transparent);
|
|
2645
|
+
}
|
|
2646
|
+
.coding-summary-card .csum-badge{
|
|
2647
|
+
font-size:11px;font-weight:700;letter-spacing:.02em;padding:3px 9px;border-radius:999px;
|
|
2648
|
+
}
|
|
2649
|
+
.coding-summary-card .csum-badge.changed{background:rgba(224,175,104,.16);color:#e0af68;border:1px solid rgba(224,175,104,.4)}
|
|
2650
|
+
.coding-summary-card .csum-badge.ok{background:rgba(158,206,106,.16);color:#9ece6a;border:1px solid rgba(158,206,106,.4)}
|
|
2651
|
+
.coding-summary-card .csum-badge.err{background:rgba(247,118,142,.16);color:#f7768e;border:1px solid rgba(247,118,142,.42)}
|
|
2652
|
+
.coding-summary-card .csum-verify{font-size:11px;font-weight:600}
|
|
2653
|
+
.coding-summary-card .csum-verify.ok{color:#9ece6a}
|
|
2654
|
+
.coding-summary-card .csum-verify.warn{color:#787c99}
|
|
2655
|
+
.coding-summary-card .csum-sec{padding:9px 13px;border-bottom:1px solid var(--border,#3b4261)}
|
|
2656
|
+
.coding-summary-card .csum-sec:last-child{border-bottom:0}
|
|
2657
|
+
.coding-summary-card .csum-lbl{
|
|
2658
|
+
font-size:10px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;
|
|
2659
|
+
color:var(--fg-dim,#565f89);margin-bottom:6px;
|
|
2660
|
+
}
|
|
2661
|
+
.coding-summary-card .csum-file{display:flex;align-items:baseline;gap:8px;
|
|
2662
|
+
font-family:var(--mono,ui-monospace,Menlo,monospace);font-size:12.5px;padding:1.5px 0}
|
|
2663
|
+
.coding-summary-card .csum-mk{color:#9ece6a;flex:0 0 auto}
|
|
2664
|
+
.coding-summary-card .csum-path{color:#7dcfff;word-break:break-all}
|
|
2665
|
+
.coding-summary-card .csum-runbox{
|
|
2666
|
+
background:var(--bg,#1a1b26);border:1px solid var(--border,#3b4261);border-radius:7px;
|
|
2667
|
+
padding:8px 10px;font-family:var(--mono,ui-monospace,Menlo,monospace);font-size:12px;
|
|
2668
|
+
}
|
|
2669
|
+
.coding-summary-card .csum-url{margin-bottom:3px}
|
|
2670
|
+
.coding-summary-card .csum-url a{color:#7dcfff;text-decoration:none}
|
|
2671
|
+
.coding-summary-card .csum-url a:hover{text-decoration:underline}
|
|
2672
|
+
.coding-summary-card .csum-cwd{color:var(--fg-dim,#565f89)}
|
|
2673
|
+
.coding-summary-card .csum-note{color:var(--fg,#c0caf5)}
|
|
2674
|
+
.coding-summary-card .csum-source{padding:2px 0;word-break:break-word}
|
|
2675
|
+
.coding-summary-card .csum-source-tool{color:#bb9af7;font-weight:700}
|
|
2676
|
+
.coding-summary-card .csum-source-detail{color:var(--fg,#c0caf5)}
|
|
2677
|
+
html[data-theme="light"] .coding-summary-card{background:#fff}
|
|
2678
|
+
html[data-theme="light"] .coding-summary-card .csum-runbox{background:#f4f5fb}
|
|
@@ -4200,6 +4200,19 @@
|
|
|
4200
4200
|
#tabbar-session-controls .session-toolbar {
|
|
4201
4201
|
background: transparent; border-bottom: none; padding: 0; z-index: auto;
|
|
4202
4202
|
}
|
|
4203
|
+
/* Wall-E coding sessions hoist their whole `.walle-session-header` here (same
|
|
4204
|
+
reparent as the Claude/Codex `.session-toolbar` above). Shed the full-width
|
|
4205
|
+
band chrome — background, bottom border, 44px min-height, padding — so it
|
|
4206
|
+
reads as an inline control cluster (cwd · msgs · model chip · nav · ⋯) on
|
|
4207
|
+
the tab strip instead of a band. The inner `.walle-header-right` already
|
|
4208
|
+
carries the chip/nav styling. */
|
|
4209
|
+
#tabbar-session-controls .walle-session-header {
|
|
4210
|
+
background: transparent; border-bottom: none; border-top: none;
|
|
4211
|
+
padding: 0; min-height: 0; height: 100%; gap: 0; flex: 0 0 auto;
|
|
4212
|
+
}
|
|
4213
|
+
#tabbar-session-controls .walle-session-header .walle-header-right { margin-left: 0; }
|
|
4214
|
+
/* The cwd · msgs meta is a touch smaller/dimmer on the dense tab strip. */
|
|
4215
|
+
#tabbar-session-controls .walle-session-header .session-meta { font-size: 11px; }
|
|
4203
4216
|
.session-toolbar-btn .tb-label { margin-left: 4px; }
|
|
4204
4217
|
/* Compact density to match the approved mock: when merged onto the tab strip,
|
|
4205
4218
|
Reflow shows icon-only and the view toggle abbreviates to Term | Conv. The
|
|
@@ -39903,7 +39916,7 @@ function _disposeSessionClientSurface(s) {
|
|
|
39903
39916
|
try {
|
|
39904
39917
|
const sid = (s && s._id) || (s && s.container && s.container.dataset && s.container.dataset.sessionId) || '';
|
|
39905
39918
|
const slot = sid && document.getElementById('tabbar-session-controls');
|
|
39906
|
-
const parked = slot && slot.querySelector(':scope > .session-toolbar[data-session-id="' + sid + '"]');
|
|
39919
|
+
const parked = slot && slot.querySelector(':scope > .session-toolbar[data-session-id="' + sid + '"], #tabbar-session-controls > .walle-session-header[data-session-id="' + sid + '"]');
|
|
39907
39920
|
if (parked) parked.remove();
|
|
39908
39921
|
} catch {}
|
|
39909
39922
|
}
|
|
@@ -43044,6 +43057,22 @@ function renderTabs(force) {
|
|
|
43044
43057
|
// (model · view toggle · reflow · ⋯) onto the right edge of the tab strip so the
|
|
43045
43058
|
// chrome reads as one row. In split view, restore each toolbar to its own pane.
|
|
43046
43059
|
// Idempotent + cheap; safe to call from renderTabs / split layout changes.
|
|
43060
|
+
// A session's hoistable toolbar lives in a different container per agent type:
|
|
43061
|
+
// - Claude/Codex/OpenCode: <.term-container data-session-id> > .session-toolbar
|
|
43062
|
+
// - Wall-E coding: <#walle-session-ID> > .walle-session-header
|
|
43063
|
+
// These two helpers paper over that so the hoist logic is agent-agnostic and
|
|
43064
|
+
// Wall-E sessions get their controls on the tab strip exactly like the rest.
|
|
43065
|
+
function _hoistHomeContainer(sid) {
|
|
43066
|
+
if (!sid) return null;
|
|
43067
|
+
return document.querySelector('.term-container[data-session-id="' + sid + '"]')
|
|
43068
|
+
|| document.getElementById('walle-session-' + sid);
|
|
43069
|
+
}
|
|
43070
|
+
function _hoistToolbarIn(cont) {
|
|
43071
|
+
if (!cont) return null;
|
|
43072
|
+
return cont.querySelector(':scope > .session-toolbar')
|
|
43073
|
+
|| cont.querySelector(':scope > .walle-session-header');
|
|
43074
|
+
}
|
|
43075
|
+
|
|
43047
43076
|
function _syncTabbarSessionControls() {
|
|
43048
43077
|
const slot = document.getElementById('tabbar-session-controls');
|
|
43049
43078
|
if (!slot) return;
|
|
@@ -43055,9 +43084,9 @@ function _syncTabbarSessionControls() {
|
|
|
43055
43084
|
// prefer its container's own child, else a copy already parked in the slot.
|
|
43056
43085
|
let keep = null;
|
|
43057
43086
|
if (activeId) {
|
|
43058
|
-
|
|
43059
|
-
|
|
43060
|
-
|| slot.querySelector(':scope > .session-
|
|
43087
|
+
keep = _hoistToolbarIn(_hoistHomeContainer(activeId))
|
|
43088
|
+
|| slot.querySelector(':scope > .session-toolbar[data-session-id="' + activeId + '"]')
|
|
43089
|
+
|| slot.querySelector(':scope > .walle-session-header[data-session-id="' + activeId + '"]');
|
|
43061
43090
|
}
|
|
43062
43091
|
// Evict every parked toolbar except the one we keep: restore it to its own pane if
|
|
43063
43092
|
// that pane has no toolbar of its own, otherwise drop it. This clears both orphans
|
|
@@ -43067,8 +43096,8 @@ function _syncTabbarSessionControls() {
|
|
|
43067
43096
|
Array.prototype.slice.call(slot.children).forEach((tb) => {
|
|
43068
43097
|
if (tb === keep) return;
|
|
43069
43098
|
const sid = tb && tb.dataset ? tb.dataset.sessionId : '';
|
|
43070
|
-
const cont =
|
|
43071
|
-
const paneHasOwnToolbar = !!(cont
|
|
43099
|
+
const cont = _hoistHomeContainer(sid);
|
|
43100
|
+
const paneHasOwnToolbar = !!_hoistToolbarIn(cont);
|
|
43072
43101
|
if (cont && !paneHasOwnToolbar) cont.insertBefore(tb, cont.firstChild);
|
|
43073
43102
|
else { try { slot.removeChild(tb); } catch {} }
|
|
43074
43103
|
});
|
|
@@ -44437,9 +44466,9 @@ function showNewSessionModal() {
|
|
|
44437
44466
|
// `custom`, which aren't LLM-branded surfaces.
|
|
44438
44467
|
var agentDefs = [
|
|
44439
44468
|
{ id: 'claude:', name: 'Claude Code', sub: 'Anthropic', color: '#D97757', agent: 'claude', detect: 'claude' },
|
|
44469
|
+
{ id: 'codex:', name: 'Codex', sub: 'OpenAI', color: '#22c55e', agent: 'codex', detect: 'codex' },
|
|
44440
44470
|
{ id: 'opencode:', name: 'OpenCode', sub: 'GLM 5.2', color: '#e0af68', agent: 'opencode', detect: 'opencode' },
|
|
44441
44471
|
{ id: 'walle:', name: 'Wall-E', sub: 'Smart routing', color: '#f59e0b', agent: 'walle', detect: null },
|
|
44442
|
-
{ id: 'codex:', name: 'Codex', sub: 'OpenAI', color: '#22c55e', agent: 'codex', detect: 'codex' },
|
|
44443
44472
|
{ id: 'cursor:', name: 'Cursor', sub: 'Anysphere', color: '#111827', accent: '#7aa2f7', agent: 'cursor', detect: 'cursor-agent' },
|
|
44444
44473
|
{ id: 'gemini:', name: 'Gemini CLI', sub: 'Google', color: '#3b82f6', agent: 'gemini', detect: 'gemini' },
|
|
44445
44474
|
{ id: 'shell:', name: 'Shell', sub: 'Terminal', color: '#6b7280', icon: '$', detect: null },
|
|
@@ -52190,7 +52219,12 @@ function onDataChanged(msg) {
|
|
|
52190
52219
|
// Reload current queue draft if viewing the same session
|
|
52191
52220
|
const sid = getQpSessionId();
|
|
52192
52221
|
if (sid) {
|
|
52193
|
-
|
|
52222
|
+
// Defer the reload while a save is in flight (avoid clobbering our own
|
|
52223
|
+
// write) OR while an inline edit is open: loadQpDraftForSession resets
|
|
52224
|
+
// qpEditingIdx and re-renders, which would close the editor mid-edit
|
|
52225
|
+
// (e.g. right after pasting an image, whose upload echoes a draft-change
|
|
52226
|
+
// broadcast back). The deferred reload runs when the edit ends.
|
|
52227
|
+
if (qpDraftSavePending(sid) || qpEditingIdx >= 0) {
|
|
52194
52228
|
qpDeferredDraftReloads.add(sid);
|
|
52195
52229
|
} else {
|
|
52196
52230
|
loadQpDraftForSession(sid);
|
|
@@ -54340,6 +54374,7 @@ function saveQpEdit(idx) {
|
|
|
54340
54374
|
}
|
|
54341
54375
|
qpEditingIdx = -1;
|
|
54342
54376
|
renderQpItems();
|
|
54377
|
+
qpProcessDeferredDraftReload();
|
|
54343
54378
|
}
|
|
54344
54379
|
|
|
54345
54380
|
async function syncQpItemToPrompt(promptId, title, content) {
|
|
@@ -54373,6 +54408,7 @@ function cancelQpEdit(idx) {
|
|
|
54373
54408
|
}
|
|
54374
54409
|
qpEditingIdx = -1;
|
|
54375
54410
|
renderQpItems();
|
|
54411
|
+
qpProcessDeferredDraftReload();
|
|
54376
54412
|
}
|
|
54377
54413
|
|
|
54378
54414
|
async function qpUploadImageBlob(blob, filename) {
|
|
@@ -54835,6 +54871,22 @@ function clearQpItems() {
|
|
|
54835
54871
|
renderQpItems();
|
|
54836
54872
|
}
|
|
54837
54873
|
|
|
54874
|
+
// Run any draft reload that was deferred while an inline edit was open. Draft
|
|
54875
|
+
// changes (our own image-paste save echo, or an external edit) are held back
|
|
54876
|
+
// during an edit so they can't reset qpEditingIdx and close the editor; this
|
|
54877
|
+
// flushes the held reload once the edit ends. A no-op while still editing, or
|
|
54878
|
+
// when a save is in flight (that save's completion runs the reload itself).
|
|
54879
|
+
function qpProcessDeferredDraftReload() {
|
|
54880
|
+
if (qpEditingIdx >= 0) return;
|
|
54881
|
+
const sid = qpCurrentSession;
|
|
54882
|
+
if (!sid) return;
|
|
54883
|
+
if (qpDraftSavePending(sid)) return;
|
|
54884
|
+
if (qpDeferredDraftReloads.has(sid)) {
|
|
54885
|
+
qpDeferredDraftReloads.delete(sid);
|
|
54886
|
+
if (!qpDraftLoading) loadQpDraftForSession(sid);
|
|
54887
|
+
}
|
|
54888
|
+
}
|
|
54889
|
+
|
|
54838
54890
|
function saveQpDraft() {
|
|
54839
54891
|
const sessionId = qpCurrentSession;
|
|
54840
54892
|
if (!sessionId) return Promise.resolve();
|
|
@@ -54871,6 +54923,12 @@ function saveQpDraft() {
|
|
|
54871
54923
|
}
|
|
54872
54924
|
qpDraftSavePendingCounts.delete(sessionId);
|
|
54873
54925
|
if (qpDeferredDraftReloads.has(sessionId)) {
|
|
54926
|
+
// Hold the deferred reload while an inline edit is open — reloading
|
|
54927
|
+
// resets qpEditingIdx and re-renders, closing the editor mid-edit
|
|
54928
|
+
// (the image-paste case: the upload's draft-save echo would otherwise
|
|
54929
|
+
// close the editor). It runs from qpProcessDeferredDraftReload() once
|
|
54930
|
+
// the edit ends.
|
|
54931
|
+
if (qpEditingIdx >= 0) return;
|
|
54874
54932
|
qpDeferredDraftReloads.delete(sessionId);
|
|
54875
54933
|
if (sessionId === qpCurrentSession && !qpDraftLoading) loadQpDraftForSession(sessionId);
|
|
54876
54934
|
}
|
|
@@ -979,6 +979,93 @@
|
|
|
979
979
|
+ '</div>';
|
|
980
980
|
};
|
|
981
981
|
|
|
982
|
+
// ------------------------------------------------------------------
|
|
983
|
+
// Coding summary card — a DETERMINISTIC, actionable wrap-up pinned above the
|
|
984
|
+
// model's prose for a completed Wall-E coding turn. Data is composed server-side
|
|
985
|
+
// (coding-run-controller buildCodingSummary): status · changed files (absolute
|
|
986
|
+
// paths) · localhost preview URLs. The model's Fixed/Unsolved/next-step prose
|
|
987
|
+
// renders below in the normal message body. Shared by Conversation + Review so
|
|
988
|
+
// the two surfaces can't drift (HARD RULE). Read-only answers still get a
|
|
989
|
+
// lightweight envelope so the user can see that no files changed and which
|
|
990
|
+
// activity/source inputs the answer used.
|
|
991
|
+
const _CSUM_STATUS = {
|
|
992
|
+
completed_with_changes: { label: 'Changed', cls: 'changed' },
|
|
993
|
+
completed_no_changes: { label: 'No file changes', cls: 'ok' },
|
|
994
|
+
read_only_answer: { label: 'Answered', cls: 'ok' },
|
|
995
|
+
blocked: { label: 'Blocked', cls: 'err' },
|
|
996
|
+
failed: { label: 'Failed', cls: 'err' },
|
|
997
|
+
};
|
|
998
|
+
MR.renderCodingSummaryHtml = function (s) {
|
|
999
|
+
if (!s || typeof s !== 'object') return '';
|
|
1000
|
+
const files = Array.isArray(s.changedFiles) ? s.changedFiles : [];
|
|
1001
|
+
const urls = Array.isArray(s.previewUrls) ? s.previewUrls : [];
|
|
1002
|
+
const isProblem = s.status === 'blocked' || s.status === 'failed';
|
|
1003
|
+
const isReadOnly = s.status === 'read_only_answer';
|
|
1004
|
+
const activity = s.activity && typeof s.activity === 'object' ? s.activity : {};
|
|
1005
|
+
const sources = Array.isArray(activity.sources) ? activity.sources : [];
|
|
1006
|
+
const toolCount = Number(activity.toolCount || 0);
|
|
1007
|
+
// Nothing actionable → no card, except read-only Wall-E coding turns where
|
|
1008
|
+
// the absence of file changes is itself the useful completion state.
|
|
1009
|
+
if (!files.length && !urls.length && !isProblem && !isReadOnly) return '';
|
|
1010
|
+
const st = _CSUM_STATUS[s.status] || { label: s.status || 'Done', cls: 'ok' };
|
|
1011
|
+
const verify = s.status === 'completed_with_changes'
|
|
1012
|
+
? (s.verified ? '<span class="csum-verify ok">verified</span>' : '<span class="csum-verify warn">unverified</span>')
|
|
1013
|
+
: '';
|
|
1014
|
+
let html = '<div class="coding-summary-card" role="group" aria-label="Coding summary">'
|
|
1015
|
+
+ '<div class="csum-top">'
|
|
1016
|
+
+ '<span class="csum-badge ' + st.cls + '">' + escHtml(st.label) + '</span>'
|
|
1017
|
+
+ verify
|
|
1018
|
+
+ '</div>';
|
|
1019
|
+
if (files.length) {
|
|
1020
|
+
html += '<div class="csum-sec"><div class="csum-lbl">What changed · '
|
|
1021
|
+
+ files.length + (files.length === 1 ? ' file' : ' files') + '</div><div class="csum-files">';
|
|
1022
|
+
for (const f of files) {
|
|
1023
|
+
const p = f && (f.abs || f.path) ? (f.abs || f.path) : String(f || '');
|
|
1024
|
+
html += '<div class="csum-file"><span class="csum-mk">•</span>'
|
|
1025
|
+
+ '<span class="csum-path">' + escHtml(p) + '</span></div>';
|
|
1026
|
+
}
|
|
1027
|
+
html += '</div></div>';
|
|
1028
|
+
} else if (isReadOnly) {
|
|
1029
|
+
html += '<div class="csum-sec"><div class="csum-lbl">What changed</div><div class="csum-runbox">'
|
|
1030
|
+
+ '<div class="csum-note">No files changed. Wall-E delivered an answer only.</div>'
|
|
1031
|
+
+ '</div></div>';
|
|
1032
|
+
}
|
|
1033
|
+
if (sources.length) {
|
|
1034
|
+
html += '<div class="csum-sec"><div class="csum-lbl">Sources checked · '
|
|
1035
|
+
+ sources.length + (sources.length === 1 ? ' item' : ' items') + '</div><div class="csum-runbox">';
|
|
1036
|
+
for (const source of sources) {
|
|
1037
|
+
const tool = source && source.tool ? source.tool : 'tool';
|
|
1038
|
+
const detail = source && source.detail ? source.detail : '';
|
|
1039
|
+
html += '<div class="csum-source"><span class="csum-source-tool">' + escHtml(tool) + '</span>'
|
|
1040
|
+
+ (detail ? '<span class="csum-source-detail"> — ' + escHtml(detail) + '</span>' : '')
|
|
1041
|
+
+ '</div>';
|
|
1042
|
+
}
|
|
1043
|
+
if (toolCount > sources.length) {
|
|
1044
|
+
html += '<div class="csum-cwd">+' + escHtml(String(toolCount - sources.length)) + ' additional activity '
|
|
1045
|
+
+ (toolCount - sources.length === 1 ? 'step' : 'steps') + '</div>';
|
|
1046
|
+
}
|
|
1047
|
+
html += '</div></div>';
|
|
1048
|
+
} else if (isReadOnly && toolCount > 0) {
|
|
1049
|
+
html += '<div class="csum-sec"><div class="csum-lbl">Activity checked</div><div class="csum-runbox">'
|
|
1050
|
+
+ '<div class="csum-note">' + escHtml(String(toolCount)) + ' activity '
|
|
1051
|
+
+ (toolCount === 1 ? 'step' : 'steps') + ' recorded.</div></div></div>';
|
|
1052
|
+
}
|
|
1053
|
+
if (urls.length) {
|
|
1054
|
+
html += '<div class="csum-sec"><div class="csum-lbl">Run & test</div><div class="csum-runbox">';
|
|
1055
|
+
for (const u of urls) {
|
|
1056
|
+
html += '<div class="csum-url">▸ <a href="' + escHtml(u) + '" target="_blank" rel="noopener noreferrer">'
|
|
1057
|
+
+ escHtml(u) + '</a></div>';
|
|
1058
|
+
}
|
|
1059
|
+
if (s.cwd) html += '<div class="csum-cwd">cwd: ' + escHtml(s.cwd) + '</div>';
|
|
1060
|
+
html += '</div></div>';
|
|
1061
|
+
} else if ((files.length || isReadOnly) && s.cwd) {
|
|
1062
|
+
html += '<div class="csum-sec"><div class="csum-lbl">Where</div><div class="csum-runbox">'
|
|
1063
|
+
+ '<div class="csum-cwd">cwd: ' + escHtml(s.cwd) + '</div></div></div>';
|
|
1064
|
+
}
|
|
1065
|
+
html += '</div>';
|
|
1066
|
+
return html;
|
|
1067
|
+
};
|
|
1068
|
+
|
|
982
1069
|
// ------------------------------------------------------------------
|
|
983
1070
|
// Tool cards: one collapsible card per tool/shell/patch/web-search call,
|
|
984
1071
|
// its result filled in (paired by metadata.callId at render time on the
|