@pellux/goodvibes-tui 1.19.6 → 1.20.0
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/CHANGELOG.md +6 -0
- package/LICENSE +21 -0
- package/README.md +143 -1613
- package/docs/README.md +4 -4
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +7 -7
- package/src/cli/ensure-goodvibes-gitignore.ts +16 -4
- package/src/cli/entrypoint.ts +5 -1
- package/src/cli/launch-auto-update.ts +63 -4
- package/src/cli/tui-startup.ts +131 -15
- package/src/config/surface.ts +21 -0
- package/src/config/tui-extension-settings.ts +9 -1
- package/src/core/conversation-line-cache.ts +1 -1
- package/src/core/conversation-rendering.ts +70 -24
- package/src/core/conversation-tool-groups.ts +23 -10
- package/src/core/conversation-types.ts +9 -0
- package/src/core/rewind-turn-anchors.ts +12 -13
- package/src/core/session-recovery.ts +59 -2
- package/src/core/session-resume-core.ts +206 -0
- package/src/core/transcript-journal.ts +32 -6
- package/src/core/turn-event-wiring.ts +55 -12
- package/src/input/command-args-hint.ts +11 -2
- package/src/input/command-registry.ts +19 -0
- package/src/input/commands/checkpoint-runtime.ts +3 -3
- package/src/input/commands/health-runtime.ts +1 -3
- package/src/input/commands/local-runtime.ts +1 -1
- package/src/input/commands/rewind-runtime.ts +132 -1
- package/src/input/commands/runtime-services.ts +11 -1
- package/src/input/commands/secret-runtime.ts +1 -1
- package/src/input/commands/session-content.ts +9 -1
- package/src/input/commands/session-picker-filter.ts +28 -0
- package/src/input/commands/session-workflow.ts +62 -73
- package/src/input/commands/session.ts +36 -1
- package/src/input/commands/shell-core.ts +57 -5
- package/src/input/commands/update-runtime.ts +120 -4
- package/src/input/commands/websearch-runtime.ts +1 -1
- package/src/input/handler-content-actions.ts +63 -39
- package/src/input/handler-feed-routes.ts +17 -3
- package/src/input/handler-feed.ts +3 -0
- package/src/input/handler-interactions.ts +19 -23
- package/src/input/handler-modal-token-routes.ts +5 -0
- package/src/input/handler-types.ts +4 -1
- package/src/input/handler-ui-state.ts +6 -2
- package/src/input/handler.ts +19 -9
- package/src/input/search.ts +94 -2
- package/src/input/selection-modal-queue.ts +176 -0
- package/src/main.ts +48 -44
- package/src/panels/builtin/operations.ts +2 -1
- package/src/panels/provider-health-domains.ts +2 -2
- package/src/renderer/block-actions-overlay.ts +59 -0
- package/src/renderer/block-actions.ts +1 -3
- package/src/renderer/context-status-hint.ts +73 -5
- package/src/renderer/conversation-layout.ts +16 -0
- package/src/renderer/conversation-overlays.ts +6 -0
- package/src/renderer/help-overlay.ts +69 -53
- package/src/renderer/selection-modal-overlay.ts +66 -10
- package/src/renderer/shell-surface.ts +15 -0
- package/src/renderer/tool-result-expanded-lines.ts +48 -0
- package/src/renderer/ui-factory.ts +1 -1
- package/src/runtime/bootstrap-command-context.ts +5 -1
- package/src/runtime/bootstrap-command-parts.ts +5 -2
- package/src/runtime/bootstrap-core.ts +3 -3
- package/src/runtime/bootstrap-hook-bridge.ts +62 -44
- package/src/runtime/bootstrap-shell.ts +31 -16
- package/src/runtime/bootstrap.ts +13 -18
- package/src/runtime/code-index-services.ts +19 -0
- package/src/runtime/durability-services.ts +11 -7
- package/src/runtime/index.ts +12 -0
- package/src/runtime/process-lifecycle.ts +63 -2
- package/src/runtime/recovery-autosave.ts +48 -0
- package/src/runtime/recovery-offer-wiring.ts +89 -0
- package/src/runtime/recovery-prompt.ts +312 -0
- package/src/runtime/resume-notice.ts +28 -93
- package/src/runtime/services.ts +14 -16
- package/src/runtime/session-liveness-marker.ts +120 -0
- package/src/runtime/session-pointer-surface.ts +22 -0
- package/src/runtime/session-resume-liveness-confirm.ts +84 -0
- package/src/runtime/session-storage-services.ts +41 -0
- package/src/runtime/workspace-checkpointing.ts +10 -2
- package/src/shell/recovery-input-helpers.ts +7 -3
- package/src/shell/retry-affordance.ts +123 -0
- package/src/shell/ui-openers.ts +9 -0
- package/src/utils/splash-lines.ts +1 -1
- package/src/version.ts +1 -1
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* renderHelpOverlay — renders the help overlay with keyboard shortcuts and slash commands.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Opened via `/commands`. `?` and `/help` open the searchable command
|
|
5
|
+
* browser (a selection modal that runs the picked command) instead — they
|
|
6
|
+
* are a different, complementary surface, not this one.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
9
|
import { type Line } from '../types/grid.ts';
|
|
@@ -68,22 +70,27 @@ export function renderHelpOverlay(
|
|
|
68
70
|
const shortcutRows: string[] = [
|
|
69
71
|
' Core Navigation',
|
|
70
72
|
' ' + '\u2500'.repeat(40),
|
|
71
|
-
` ${'Up / Down'.padEnd(20)}
|
|
73
|
+
` ${'Up / Down'.padEnd(20)} Recall input history; Down at bottom focuses the process indicator`,
|
|
72
74
|
` ${'PageUp / PageDn'.padEnd(20)} Scroll by full page`,
|
|
73
75
|
` ${kb('search').padEnd(20)} Search conversation (Ctrl+F)`,
|
|
74
76
|
'',
|
|
75
77
|
' Prompt And Editing',
|
|
76
78
|
' ' + '\u2500'.repeat(40),
|
|
77
|
-
` ${'Enter'.padEnd(20)} Submit message`,
|
|
79
|
+
` ${'Enter'.padEnd(20)} Submit message (composer empty: open the block-actions menu)`,
|
|
78
80
|
` ${'Shift+Enter'.padEnd(20)} Insert newline`,
|
|
79
81
|
` ${kb('paste').padEnd(20)} Paste (image priority)`,
|
|
80
82
|
` ${(kb('undo') + ' / ' + kb('redo')).padEnd(20)} Undo / redo`,
|
|
83
|
+
shortcutLine('(paste >8 lines)', 'Folds to [TEXT: pN, M lines] — /pastes previews it before you submit'),
|
|
81
84
|
'',
|
|
82
85
|
' Overlays And Panels',
|
|
83
86
|
' ' + '\u2500'.repeat(40),
|
|
84
|
-
shortcutLine('?', '
|
|
85
|
-
shortcutLine('/
|
|
86
|
-
shortcutLine('
|
|
87
|
+
shortcutLine('? or /help', 'Open the command browser (search & run any command)'),
|
|
88
|
+
shortcutLine('/commands', 'This reference: keyboard shortcuts + full command list'),
|
|
89
|
+
shortcutLine('/shortcuts', 'Keyboard-shortcuts-only reference (a separate, generated overlay)'),
|
|
90
|
+
// Precedence (highest first): a visible panel workspace claims Tab for
|
|
91
|
+
// focus-swap; failing that, a partial path under the cursor claims it for
|
|
92
|
+
// completion; only then does it fall through to collapse/expand.
|
|
93
|
+
shortcutLine('Tab', 'Panel focus-swap, else path-complete, else collapse/expand block'),
|
|
87
94
|
...panelBindingRows,
|
|
88
95
|
'',
|
|
89
96
|
// The shared in-panel contract every workspace panel honors. These are not
|
|
@@ -149,7 +156,7 @@ export function renderHelpOverlay(
|
|
|
149
156
|
['sessions', 'Browse and resume saved sessions'],
|
|
150
157
|
['save', 'Save the current session'],
|
|
151
158
|
['compact', 'Compact the conversation history'],
|
|
152
|
-
['clear', 'Clear the conversation'],
|
|
159
|
+
['clear', 'Clear the conversation display (keeps LLM context)'],
|
|
153
160
|
['keybindings', 'List and customize key bindings'],
|
|
154
161
|
['panel', 'Open, focus, or manage panels'],
|
|
155
162
|
];
|
|
@@ -197,10 +204,11 @@ export function renderHelpOverlay(
|
|
|
197
204
|
}
|
|
198
205
|
} else if (!hasCommand('help')) {
|
|
199
206
|
commandRows.push('', ' Essentials', ' ' + '\u2500'.repeat(40));
|
|
200
|
-
commandRows.push(' /
|
|
207
|
+
commandRows.push(' /commands Show this help overlay');
|
|
208
|
+
commandRows.push(' /help Browse & run any command');
|
|
201
209
|
commandRows.push(' /shortcuts Keyboard shortcut reference');
|
|
202
210
|
commandRows.push(' /model Select LLM model');
|
|
203
|
-
commandRows.push(' /clear Clear conversation');
|
|
211
|
+
commandRows.push(' /clear Clear the conversation display (keeps LLM context)');
|
|
204
212
|
}
|
|
205
213
|
|
|
206
214
|
const allRows = [...shortcutRows, ...commandRows];
|
|
@@ -255,61 +263,69 @@ export function renderShortcutsOverlay(
|
|
|
255
263
|
return ` ${keyCol} ${desc}`;
|
|
256
264
|
}
|
|
257
265
|
|
|
258
|
-
//
|
|
259
|
-
|
|
266
|
+
// Keys that are real, but not KeyAction entries in keybindingsManager
|
|
267
|
+
// (context-dependent chords, or literal characters rather than bindable
|
|
268
|
+
// actions) \u2014 a maintained table rather than hand-picked getAll() calls, so
|
|
269
|
+
// adding a new KeyAction automatically shows up below without anyone
|
|
270
|
+
// remembering to also add a row for it here.
|
|
271
|
+
const HARDCODED_ROWS: ReadonlyArray<[key: string, desc: string]> = [
|
|
272
|
+
['Up / Down', 'Recall input history; Down at bottom focuses the process indicator'],
|
|
273
|
+
['PageUp / PageDn', 'Scroll by full page'],
|
|
274
|
+
['Home / End', 'Jump to start / end of line'],
|
|
275
|
+
['n / N (search)', 'Next / previous match'],
|
|
276
|
+
['Mouse wheel', 'Scroll conversation or hovered panel'],
|
|
277
|
+
['Enter', 'Submit message (composer empty: open the block-actions menu)'],
|
|
278
|
+
['Shift+Enter', 'Insert newline'],
|
|
279
|
+
['@', 'Open file picker'],
|
|
280
|
+
['/', 'Slash command mode'],
|
|
281
|
+
['Esc', 'Close overlay / cancel generation / clear prompt (context-dependent)'],
|
|
282
|
+
['Shift+Tab', 'Cycle permission mode (auto-approve / prompt / manual)'],
|
|
283
|
+
['F2', 'Open the Fleet panel'],
|
|
284
|
+
['? or /help', 'Open the command browser (search & run any command)'],
|
|
285
|
+
// Precedence (highest first): panel focus-swap, else path-completion,
|
|
286
|
+
// else collapse/expand \u2014 see the Panels section's Tab row below for the
|
|
287
|
+
// full chain; this row is the "what does it do here" short form.
|
|
288
|
+
['Tab', 'Collapse/expand block (composer empty \u2014 see Panels: Tab for full precedence)'],
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
const IN_PANEL_ROWS: ReadonlyArray<[key: string, desc: string]> = [
|
|
292
|
+
['j / k', 'Move selection down / up'],
|
|
293
|
+
['g / G', 'Jump to top / bottom'],
|
|
294
|
+
['/', 'Filter the list'],
|
|
295
|
+
];
|
|
296
|
+
|
|
297
|
+
// Generated from the live keybindings table (default bindings AND any user
|
|
298
|
+
// overrides) so this listing can never drift from what a key actually
|
|
299
|
+
// does \u2014 no action is hand-picked, none can be silently missed.
|
|
300
|
+
const allBindings = keybindingsManager.getAll();
|
|
301
|
+
const panelActionRows = allBindings
|
|
302
|
+
.filter((entry) => entry.action.startsWith('panel-'))
|
|
303
|
+
.map((entry) => row(entry.combos.map((c) => keybindingsManager.formatCombo(c)).join(', '), entry.description));
|
|
304
|
+
const otherActionRows = allBindings
|
|
305
|
+
.filter((entry) => !entry.action.startsWith('panel-'))
|
|
306
|
+
.map((entry) => row(entry.combos.map((c) => keybindingsManager.formatCombo(c)).join(', '), entry.description));
|
|
260
307
|
|
|
261
308
|
const allRows: string[] = [
|
|
262
|
-
' Navigation',
|
|
263
|
-
' ' + '\u2500'.repeat(40),
|
|
264
|
-
row('Up / Down', 'Scroll / history recall'),
|
|
265
|
-
row('PageUp / PageDn', 'Scroll by full page'),
|
|
266
|
-
row('Home / End', 'Jump to start / end of line'),
|
|
267
|
-
row(kb('search'), 'Search conversation'),
|
|
268
|
-
row('n / N (search)', 'Next / previous match'),
|
|
269
|
-
row('Mouse wheel', 'Scroll conversation or hovered panel'),
|
|
270
|
-
'',
|
|
271
|
-
' Editing',
|
|
309
|
+
' Navigation & Editing',
|
|
272
310
|
' ' + '\u2500'.repeat(40),
|
|
273
|
-
row(
|
|
274
|
-
row('Shift+Enter', 'Insert newline'),
|
|
275
|
-
row('@', 'Open file picker'),
|
|
276
|
-
row('/', 'Slash command mode'),
|
|
277
|
-
row(kb('paste'), 'Paste (image priority)'),
|
|
278
|
-
row(`${kb('undo')} / ${kb('redo')}`, 'Undo / redo'),
|
|
279
|
-
row(kb('clear-prompt'), 'Clear prompt'),
|
|
280
|
-
row(kb('delete-word'), 'Delete word backward'),
|
|
281
|
-
row(kb('kill-line'), 'Kill to end of line'),
|
|
282
|
-
row(kb('apply-diff-line-start'), 'Apply diff / line start'),
|
|
283
|
-
row(kb('next-error-line-end'), 'Next error / line end'),
|
|
311
|
+
...HARDCODED_ROWS.map(([key, desc]) => row(key, desc)),
|
|
284
312
|
'',
|
|
285
|
-
' Actions',
|
|
313
|
+
' Actions & Shortcuts (customize via /keybindings)',
|
|
286
314
|
' ' + '\u2500'.repeat(40),
|
|
287
|
-
|
|
288
|
-
row(kb('bookmark'), 'Bookmark block'),
|
|
289
|
-
row(kb('block-copy'), 'Copy block to clipboard'),
|
|
290
|
-
row(kb('block-save'), 'Save block to file'),
|
|
291
|
-
row(kb('copy-selection'), 'Copy selection'),
|
|
292
|
-
row('F2', 'Open the Fleet panel'),
|
|
293
|
-
row('?', 'Help overlay'),
|
|
294
|
-
row(`${kb('clear-cancel')} x2`, 'Exit'),
|
|
315
|
+
...otherActionRows,
|
|
295
316
|
'',
|
|
296
317
|
' Panels',
|
|
297
318
|
' ' + '\u2500'.repeat(40),
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
row(
|
|
303
|
-
|
|
304
|
-
row(kb('panel-close'), 'Close active panel'),
|
|
305
|
-
row(kb('panel-close-all'), 'Close all panels'),
|
|
306
|
-
row(kb('panel-ops'), 'Open and focus the Fleet panel'),
|
|
319
|
+
// Full Tab precedence, stated once here as the authoritative chain: a
|
|
320
|
+
// visible panel workspace claims Tab for focus-swap first; failing that,
|
|
321
|
+
// a partial path under the cursor claims it for completion; only then
|
|
322
|
+
// does it fall through to collapse/expand (the Actions section's row).
|
|
323
|
+
row('Tab', 'Panel focus-swap (1st) > path-complete (2nd) > collapse/expand block (3rd)'),
|
|
324
|
+
...panelActionRows,
|
|
307
325
|
'',
|
|
308
326
|
' In-Panel Controls',
|
|
309
327
|
' ' + '\u2500'.repeat(40),
|
|
310
|
-
|
|
311
|
-
row('g / G', 'Jump to top / bottom'),
|
|
312
|
-
row('/', 'Filter the list'),
|
|
328
|
+
...IN_PANEL_ROWS.map(([key, desc]) => row(key, desc)),
|
|
313
329
|
'',
|
|
314
330
|
` Config: /keybindings to list and customize`,
|
|
315
331
|
];
|
|
@@ -69,6 +69,41 @@ function rowCostFor(item: SelectionItem, remaining: number): number {
|
|
|
69
69
|
return 1 + (computeDetailLayout(item, remaining).wrappedDetail?.length ?? 0);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
const MODAL_MARGIN = 4;
|
|
73
|
+
const MODAL_MAX_WIDTH = 72;
|
|
74
|
+
const INDICATOR_WIDTH = 2;
|
|
75
|
+
/** Rows kept clear between the box and the top/bottom of the terminal. */
|
|
76
|
+
const VIEWPORT_INSET_ROWS = 2;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Rows the box spends on its own frame, counted as they are actually pushed
|
|
80
|
+
* below: top and bottom border, the title, the section row (a search label,
|
|
81
|
+
* search input and divider when the modal searches), the "Results" header and
|
|
82
|
+
* the footer hint. `chromeRows` is the budget-math constant the surface
|
|
83
|
+
* metrics subtract from a total; this is the physical count, and it is what
|
|
84
|
+
* decides how tall the box may really grow.
|
|
85
|
+
*/
|
|
86
|
+
function frameRowsFor(modal: SelectionModal): number {
|
|
87
|
+
return modal.allowSearch ? 8 : 6;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Physical rows every row of the list needs, category headers included — the
|
|
92
|
+
* height the box would have to give the list for nothing to be scrolled away.
|
|
93
|
+
*/
|
|
94
|
+
function totalContentRowsFor(modal: SelectionModal, remaining: number): number {
|
|
95
|
+
let rows = 0;
|
|
96
|
+
let lastCategory: string | undefined;
|
|
97
|
+
for (const item of modal.filteredItems) {
|
|
98
|
+
if (item.category && item.category !== lastCategory) {
|
|
99
|
+
lastCategory = item.category;
|
|
100
|
+
rows += 1;
|
|
101
|
+
}
|
|
102
|
+
rows += rowCostFor(item, remaining);
|
|
103
|
+
}
|
|
104
|
+
return rows;
|
|
105
|
+
}
|
|
106
|
+
|
|
72
107
|
/**
|
|
73
108
|
* Render the selection modal as Line[] for overlay in the viewport.
|
|
74
109
|
*/
|
|
@@ -79,9 +114,29 @@ export function renderSelectionModalOverlay(
|
|
|
79
114
|
): Line[] {
|
|
80
115
|
const lines: Line[] = [];
|
|
81
116
|
const chromeRows = modal.allowSearch ? 5 : 4;
|
|
117
|
+
// Width does not depend on the row budget, so the box layout is settled
|
|
118
|
+
// first: the list's real height can only be measured once the wrap width is
|
|
119
|
+
// known. `createOverlayBoxLayout` resolves the requested max width the same
|
|
120
|
+
// way `getOverlaySurfaceMetrics` does, so both agree on the box.
|
|
121
|
+
const layout = createOverlayBoxLayout(width, MODAL_MARGIN, MODAL_MAX_WIDTH);
|
|
122
|
+
const remaining = layout.innerWidth - INDICATOR_WIDTH;
|
|
123
|
+
|
|
124
|
+
// Size to content before falling back to the viewport's height ratio. When
|
|
125
|
+
// every row (and its wrapped detail) fits in the space the terminal
|
|
126
|
+
// actually has, ask for exactly that many rows, so a short list is never
|
|
127
|
+
// split behind a "(n below)" hint with screen still empty below the box —
|
|
128
|
+
// a two-item question in particular must always show both its answers. A
|
|
129
|
+
// list genuinely taller than the terminal keeps the ratio-derived budget
|
|
130
|
+
// and scrolls as before.
|
|
131
|
+
const neededContentRows = totalContentRowsFor(modal, remaining);
|
|
132
|
+
const fittableContentRows = viewportHeight - VIEWPORT_INSET_ROWS - frameRowsFor(modal);
|
|
133
|
+
const growToFitTotalRows = neededContentRows > 0 && neededContentRows <= fittableContentRows
|
|
134
|
+
? neededContentRows + chromeRows
|
|
135
|
+
: undefined;
|
|
136
|
+
|
|
82
137
|
const metrics = getOverlaySurfaceMetrics(width, viewportHeight, {
|
|
83
|
-
margin:
|
|
84
|
-
maxWidth:
|
|
138
|
+
margin: MODAL_MARGIN,
|
|
139
|
+
maxWidth: MODAL_MAX_WIDTH,
|
|
85
140
|
chromeRows,
|
|
86
141
|
minContentRows: 6,
|
|
87
142
|
// Modals size to content: items whose detail wraps onto extra lines can
|
|
@@ -89,8 +144,11 @@ export function renderSelectionModalOverlay(
|
|
|
89
144
|
// ceiling grows with the viewport rather than staying pinned at a small
|
|
90
145
|
// constant (still bounded — never larger than the terminal can show).
|
|
91
146
|
maxContentRows: Math.max(10, viewportHeight - chromeRows - 4),
|
|
147
|
+
// A floor only. The shared budget keeps its own target and ceiling, so a
|
|
148
|
+
// modal whose content is shorter than that target is unaffected: this can
|
|
149
|
+
// raise the budget to fit the list, never lower it.
|
|
150
|
+
minTotalRows: growToFitTotalRows,
|
|
92
151
|
});
|
|
93
|
-
const layout = createOverlayBoxLayout(width, metrics.margin, metrics.boxWidth);
|
|
94
152
|
|
|
95
153
|
lines.push(createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.topLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.topRight, BORDER_FG, DEFAULT_OVERLAY_PALETTE.titleBg));
|
|
96
154
|
|
|
@@ -143,8 +201,6 @@ export function renderSelectionModalOverlay(
|
|
|
143
201
|
putText(line, layout.margin + 2, layout.innerWidth, fitDisplay(message, layout.innerWidth), { fg: MUTED_FG, dim: true });
|
|
144
202
|
lines.push(line);
|
|
145
203
|
} else {
|
|
146
|
-
const indicatorWidth = 2;
|
|
147
|
-
const remaining = layout.innerWidth - indicatorWidth;
|
|
148
204
|
const rowBudget = Math.max(1, metrics.contentRows);
|
|
149
205
|
|
|
150
206
|
// Row-budget-aware windowing: rather than assuming one physical row per
|
|
@@ -210,12 +266,12 @@ export function renderSelectionModalOverlay(
|
|
|
210
266
|
const detailLayout = computeDetailLayout(item, remaining);
|
|
211
267
|
const labelWidth = detailLayout.labelWidth;
|
|
212
268
|
const labelLine = createOverlayContentLine(width, layout, BORDER_FG, isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg);
|
|
213
|
-
putText(labelLine, layout.margin + 2,
|
|
269
|
+
putText(labelLine, layout.margin + 2, INDICATOR_WIDTH, indicator, {
|
|
214
270
|
fg: isSelected ? TITLE_FG : MUTED_FG,
|
|
215
271
|
bg: isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg,
|
|
216
272
|
bold: isSelected,
|
|
217
273
|
});
|
|
218
|
-
putText(labelLine, layout.margin + 2 +
|
|
274
|
+
putText(labelLine, layout.margin + 2 + INDICATOR_WIDTH, labelWidth, fitDisplay(truncateDisplay(item.label, labelWidth), labelWidth), {
|
|
219
275
|
fg: labelColor,
|
|
220
276
|
bg: isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg,
|
|
221
277
|
bold: isSelected,
|
|
@@ -223,11 +279,11 @@ export function renderSelectionModalOverlay(
|
|
|
223
279
|
if (item.detail) {
|
|
224
280
|
if (detailLayout.wrappedDetail === null) {
|
|
225
281
|
// Fits beside the label at a readable width — no truncation needed.
|
|
226
|
-
putText(labelLine, layout.margin + 2 +
|
|
282
|
+
putText(labelLine, layout.margin + 2 + INDICATOR_WIDTH + labelWidth, 2, ' ', {
|
|
227
283
|
fg: BODY_FG,
|
|
228
284
|
bg: isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg,
|
|
229
285
|
});
|
|
230
|
-
putText(labelLine, layout.margin + 2 +
|
|
286
|
+
putText(labelLine, layout.margin + 2 + INDICATOR_WIDTH + labelWidth + 2, detailLayout.detailWidth, fitDisplay(item.detail, detailLayout.detailWidth), {
|
|
231
287
|
fg: detailColor,
|
|
232
288
|
bg: isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg,
|
|
233
289
|
});
|
|
@@ -238,7 +294,7 @@ export function renderSelectionModalOverlay(
|
|
|
238
294
|
lines.push(labelLine);
|
|
239
295
|
for (const detailLineText of detailLayout.wrappedDetail) {
|
|
240
296
|
const detailLine = createOverlayContentLine(width, layout, BORDER_FG, isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg);
|
|
241
|
-
putText(detailLine, layout.margin + 2 +
|
|
297
|
+
putText(detailLine, layout.margin + 2 + INDICATOR_WIDTH, remaining, fitDisplay(detailLineText, remaining), {
|
|
242
298
|
fg: detailColor,
|
|
243
299
|
bg: isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg,
|
|
244
300
|
dim: !isSelected,
|
|
@@ -53,6 +53,14 @@ export interface ShellFooterBuildOptions {
|
|
|
53
53
|
* below the context pressure hint.
|
|
54
54
|
*/
|
|
55
55
|
readonly scriptableStatusLine?: string | null;
|
|
56
|
+
/**
|
|
57
|
+
* The one-key retry/switch-model affordance's transient hint text (see
|
|
58
|
+
* retry-affordance.ts). Rendered as its own line ABOVE the context
|
|
59
|
+
* pressure hint while armed; present here only means "show it now" — the
|
|
60
|
+
* caller is responsible for passing null the instant it disarms, so this
|
|
61
|
+
* is a time-bounded status line, never a lingering claim.
|
|
62
|
+
*/
|
|
63
|
+
readonly retryHint?: string | null;
|
|
56
64
|
/**
|
|
57
65
|
* Compact footer posture for short terminals (~<30 rows). Collapses the
|
|
58
66
|
* footer to its essentials (prompt box + token/cost line + help) and drops
|
|
@@ -173,6 +181,13 @@ export function buildShellFooter(
|
|
|
173
181
|
const hintLine = UIFactory.stringToLine(options.contextStatusHint, options.width, { fg: DIM_STATUS_FG });
|
|
174
182
|
lines.unshift(hintLine);
|
|
175
183
|
}
|
|
184
|
+
// Retry affordance — topmost of the passive hint lines while armed (an
|
|
185
|
+
// actionable, time-bounded prompt outranks the passive status hints
|
|
186
|
+
// below it); simply absent the instant it disarms.
|
|
187
|
+
if (options.retryHint) {
|
|
188
|
+
const retryLine = UIFactory.stringToLine(options.retryHint, options.width, { fg: DIM_STATUS_FG, bold: true });
|
|
189
|
+
lines.unshift(retryLine);
|
|
190
|
+
}
|
|
176
191
|
}
|
|
177
192
|
lastRenderedFooterHeight = { compact: options.compact ?? false, height: lines.length };
|
|
178
193
|
return { lines, height: lines.length };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool-result-expanded-lines.ts — the single source of truth for how many
|
|
3
|
+
* screen lines a tool-result message's EXPANDED form actually renders to.
|
|
4
|
+
*
|
|
5
|
+
* Tool-result content gets pretty-printed (JSON.stringify(parsed, null, 2))
|
|
6
|
+
* or diff-rendered before display, so the raw message's `content.split('\n')`
|
|
7
|
+
* length is not what the user sees once expanded — a one-line JSON blob can
|
|
8
|
+
* pretty-print to 50 lines. Both the per-block "N lines" badge
|
|
9
|
+
* (conversation-rendering.ts) and the folded-group total (conversation-tool-
|
|
10
|
+
* groups.ts) must count the SAME post-render lines, or the two disagree with
|
|
11
|
+
* each other and both can disagree with what Tab actually reveals.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Line } from '../types/grid.ts';
|
|
15
|
+
import { renderMarkdownTracked } from './markdown.ts';
|
|
16
|
+
import { renderDiffView } from './diff-view.ts';
|
|
17
|
+
|
|
18
|
+
/** True when `content` looks like a unified diff (matches the detection used
|
|
19
|
+
* by renderConversationToolMessage to pick the diff block type). */
|
|
20
|
+
export function isDiffContent(content: string): boolean {
|
|
21
|
+
const contentLines = content.split('\n');
|
|
22
|
+
const hasDiffHeader = contentLines.some((l) => l.startsWith('--- ')) && contentLines.some((l) => l.startsWith('+++ '));
|
|
23
|
+
const hasHunk = contentLines.some((l) => l.startsWith('@@ '));
|
|
24
|
+
return hasDiffHeader && hasHunk;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Render a tool result's content exactly as its EXPANDED form would appear in
|
|
29
|
+
* the transcript (diff view for diffs; pretty-printed JSON, when parseable,
|
|
30
|
+
* for everything else). The caller decides whether to actually display these
|
|
31
|
+
* lines or just count them.
|
|
32
|
+
*/
|
|
33
|
+
export function renderExpandedToolResultLines(content: string, width: number): Line[] {
|
|
34
|
+
if (isDiffContent(content)) {
|
|
35
|
+
return renderDiffView(content, width);
|
|
36
|
+
}
|
|
37
|
+
let contentToRender = content;
|
|
38
|
+
const trimmed = contentToRender.trimStart();
|
|
39
|
+
if ((trimmed.startsWith('{') || trimmed.startsWith('[')) && contentToRender.length < 100_000) {
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(contentToRender);
|
|
42
|
+
contentToRender = `\`\`\`json\n${JSON.stringify(parsed, null, 2)}\n\`\`\``;
|
|
43
|
+
} catch {
|
|
44
|
+
// Leave invalid JSON as-is — falls through to the plain markdown render below.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return renderMarkdownTracked(contentToRender, width).lines;
|
|
48
|
+
}
|
|
@@ -624,7 +624,7 @@ export class UIFactory {
|
|
|
624
624
|
// them; keep the elapsed timer since "how long the approval has waited" is honest.
|
|
625
625
|
const speedSuffix = (!approvalPending && tokenSpeed !== undefined && tokenSpeed > 0) ? ` (${Math.round(tokenSpeed)} tok/s)` : '';
|
|
626
626
|
const elapsedSuffix = elapsedMs !== undefined ? ` (${formatElapsed(elapsedMs)})` : '';
|
|
627
|
-
const ttftSuffix = (!approvalPending && ttftMs !== undefined && ttftMs > 0) ? `
|
|
627
|
+
const ttftSuffix = (!approvalPending && ttftMs !== undefined && ttftMs > 0) ? ` (first token ${formatElapsed(ttftMs)})` : '';
|
|
628
628
|
const text = ` ${spinner} ${phrase}${speedSuffix}${elapsedSuffix}${ttftSuffix} `;
|
|
629
629
|
|
|
630
630
|
const textWidth = Math.max(1, getDisplayWidth(text) - 1);
|
|
@@ -30,7 +30,7 @@ import type { HookWorkbench } from '@pellux/goodvibes-sdk/platform/hooks';
|
|
|
30
30
|
import type { WorktreeRegistry } from '@/runtime/index.ts';
|
|
31
31
|
import type { SandboxSessionRegistry } from '@/runtime/index.ts';
|
|
32
32
|
import type { UiReadModels } from './ui-read-models.ts';
|
|
33
|
-
import type { ShellPathService } from '@/runtime/index.ts';
|
|
33
|
+
import type { ShellPathService, SessionSurface } from '@/runtime/index.ts';
|
|
34
34
|
import type {
|
|
35
35
|
ShellAgentManagerService,
|
|
36
36
|
ShellAutomationManagerRuntimeService,
|
|
@@ -76,6 +76,8 @@ export type CreateBootstrapCommandContextOptions = {
|
|
|
76
76
|
policyRuntimeState: PolicyRuntimeState;
|
|
77
77
|
readModels: UiReadModels;
|
|
78
78
|
shellPaths: ShellPathService;
|
|
79
|
+
/** The runtime's declare-once session-storage handle (runtime/services.ts). */
|
|
80
|
+
surface: SessionSurface;
|
|
79
81
|
remoteRuntime?: RemoteCommandService;
|
|
80
82
|
planRuntime?: PlanRuntimeService;
|
|
81
83
|
fileUndoManager: FileUndoManager;
|
|
@@ -163,6 +165,7 @@ export function createBootstrapCommandContext(
|
|
|
163
165
|
policyRuntimeState,
|
|
164
166
|
readModels,
|
|
165
167
|
shellPaths,
|
|
168
|
+
surface,
|
|
166
169
|
remoteRuntime,
|
|
167
170
|
planRuntime,
|
|
168
171
|
fileUndoManager,
|
|
@@ -277,6 +280,7 @@ export function createBootstrapCommandContext(
|
|
|
277
280
|
benchmarkStore,
|
|
278
281
|
});
|
|
279
282
|
const workspace = createBootstrapCommandWorkspaceSection({
|
|
283
|
+
surface,
|
|
280
284
|
keybindingsManager,
|
|
281
285
|
fileUndoManager,
|
|
282
286
|
workspaceCheckpointManager,
|
|
@@ -32,7 +32,7 @@ import type { HookWorkbench } from '@pellux/goodvibes-sdk/platform/hooks';
|
|
|
32
32
|
import type { WorktreeRegistry } from '@/runtime/index.ts';
|
|
33
33
|
import type { SandboxSessionRegistry } from '@/runtime/index.ts';
|
|
34
34
|
import type { UiReadModels } from './ui-read-models.ts';
|
|
35
|
-
import type { ShellPathService } from '@/runtime/index.ts';
|
|
35
|
+
import type { ShellPathService, SessionSurface } from '@/runtime/index.ts';
|
|
36
36
|
import type {
|
|
37
37
|
ShellAgentManagerService,
|
|
38
38
|
ShellAutomationManagerRuntimeService,
|
|
@@ -95,6 +95,8 @@ export interface BootstrapCommandSectionOptions {
|
|
|
95
95
|
readonly policyRuntimeState: PolicyRuntimeState;
|
|
96
96
|
readonly readModels: UiReadModels;
|
|
97
97
|
readonly shellPaths: ShellPathService;
|
|
98
|
+
/** The runtime's declare-once session-storage handle (runtime/services.ts). */
|
|
99
|
+
readonly surface: SessionSurface;
|
|
98
100
|
readonly fileUndoManager: FileUndoManager;
|
|
99
101
|
readonly workspaceCheckpointManager?: WorkspaceCheckpointManager;
|
|
100
102
|
readonly gatewayMethods?: GatewayMethodCatalog;
|
|
@@ -372,12 +374,13 @@ export function createBootstrapCommandProviderSection(
|
|
|
372
374
|
export function createBootstrapCommandWorkspaceSection(
|
|
373
375
|
options: Pick<
|
|
374
376
|
BootstrapCommandSectionOptions,
|
|
375
|
-
'keybindingsManager' | 'fileUndoManager' | 'workspaceCheckpointManager' | 'gatewayMethods' | 'workspaceTrustManager' | 'workspaceRegistrationManager' | 'panelManager' | 'profileManager' | 'bookmarkManager'
|
|
377
|
+
'surface' | 'keybindingsManager' | 'fileUndoManager' | 'workspaceCheckpointManager' | 'gatewayMethods' | 'workspaceTrustManager' | 'workspaceRegistrationManager' | 'panelManager' | 'profileManager' | 'bookmarkManager'
|
|
376
378
|
| 'projectPlanningService' | 'projectPlanningProjectId' | 'workPlanStore'
|
|
377
379
|
>,
|
|
378
380
|
shellServices: BootstrapCommandShellServices,
|
|
379
381
|
): BootstrapCommandWorkspaceSection {
|
|
380
382
|
return {
|
|
383
|
+
surface: options.surface,
|
|
381
384
|
keybindingsManager: options.keybindingsManager,
|
|
382
385
|
fileUndoManager: options.fileUndoManager,
|
|
383
386
|
workspaceCheckpointManager: options.workspaceCheckpointManager,
|
|
@@ -350,7 +350,7 @@ export async function initializeBootstrapCore(
|
|
|
350
350
|
|
|
351
351
|
const toolRegistry = new ToolRegistry();
|
|
352
352
|
const { fileCache, projectIndex } = registerAllTools(toolRegistry, {
|
|
353
|
-
surfaceRoot:
|
|
353
|
+
surfaceRoot: services.surface.surfaceRoot,
|
|
354
354
|
localhostFetchApproval: services.localhostFetchApproval, // loopback-fetch ask, built once in the services composition
|
|
355
355
|
execPromptAnswerHandler: services.execPromptAnswerHandler, // terminal prompt-answer path, built once in the services composition
|
|
356
356
|
fileUndoManager: services.fileUndoManager,
|
|
@@ -384,7 +384,7 @@ export async function initializeBootstrapCore(
|
|
|
384
384
|
});
|
|
385
385
|
// Note: installWrfcAgentToolGuard is called after routeOrBuffer is defined (further below) so the onTrace callback routes guard decisions through the pre-router buffer.
|
|
386
386
|
services.agentOrchestrator.setDependencies({
|
|
387
|
-
surfaceRoot:
|
|
387
|
+
surfaceRoot: services.surface.surfaceRoot,
|
|
388
388
|
// setDependencies is a wholesale replace: re-install the prompt-answer handler and the loopback-fetch ask here or this rewire silently drops them.
|
|
389
389
|
execPromptAnswerHandler: services.execPromptAnswerHandler,
|
|
390
390
|
localhostFetchApproval: services.localhostFetchApproval,
|
|
@@ -507,7 +507,7 @@ export async function initializeBootstrapCore(
|
|
|
507
507
|
|
|
508
508
|
// ── WRFC chain persistence ──────────────────────────────────────────────────────────
|
|
509
509
|
const wrfcPersistence = createWrfcPersistence({
|
|
510
|
-
snapshotPath: join(workingDir, '.goodvibes',
|
|
510
|
+
snapshotPath: join(workingDir, '.goodvibes', services.surface.surfaceRoot, 'wrfc-chains.json'),
|
|
511
511
|
getSystemMessageRouter: () => systemMessageRouterRef.value,
|
|
512
512
|
controller: services.wrfcController,
|
|
513
513
|
});
|
|
@@ -14,7 +14,8 @@ import type { SessionManager } from '@pellux/goodvibes-sdk/platform/sessions';
|
|
|
14
14
|
import type { PanelManager } from '../panels/panel-manager.ts';
|
|
15
15
|
import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
|
|
16
16
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
17
|
-
import {
|
|
17
|
+
import { resumeSessionCore } from '../core/session-resume-core.ts';
|
|
18
|
+
import type { SessionSurface } from '@/runtime/index.ts';
|
|
18
19
|
|
|
19
20
|
export interface ResumeSessionOptions {
|
|
20
21
|
readonly runtimeBus: RuntimeEventBus;
|
|
@@ -31,77 +32,94 @@ export interface ResumeSessionOptions {
|
|
|
31
32
|
readonly hookDispatcher: HookDispatcher;
|
|
32
33
|
readonly sessionManager: SessionManager;
|
|
33
34
|
readonly panelManager: PanelManager;
|
|
35
|
+
/** The app's declare-once session-storage handle — the same one /session resume threads. */
|
|
36
|
+
readonly surface: SessionSurface;
|
|
37
|
+
/**
|
|
38
|
+
* Multi-instance guard for this seam. Resolves false when the operator
|
|
39
|
+
* declines to fork a session another terminal still has open. Optional so a
|
|
40
|
+
* host without a modal surface keeps the seam's original behavior.
|
|
41
|
+
*/
|
|
42
|
+
readonly confirmLiveResume?: (sessionId: string) => Promise<boolean>;
|
|
34
43
|
readonly configManager: Pick<ConfigManager, 'get' | 'getCategory'>;
|
|
35
44
|
readonly providerRegistry: Pick<ProviderRegistry, 'get' | 'getCurrentModel' | 'getForModel' | 'require' | 'resolveModelPricing'>;
|
|
36
|
-
readonly homeDirectory: string;
|
|
37
45
|
/** See CommandSessionServices.hydrateSessionUsage (command-registry.ts). */
|
|
38
46
|
readonly hydrateSessionUsage?: () => void;
|
|
47
|
+
/**
|
|
48
|
+
* Reselects the resumed session's saved model through the live provider
|
|
49
|
+
* registry (matches session-workflow.ts's `/session resume` reselection
|
|
50
|
+
* fallback — see core/session-resume-core.ts). Optional so a caller without
|
|
51
|
+
* a provider API wired still gets the direct-assignment behavior this seam
|
|
52
|
+
* always had.
|
|
53
|
+
*/
|
|
54
|
+
readonly selectModel?: (model: string) => Promise<{ readonly registryKey: string; readonly providerId: string }>;
|
|
39
55
|
}
|
|
40
56
|
|
|
41
|
-
|
|
42
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Delegates the mechanical resume sequence to resumeSessionCore (the same
|
|
59
|
+
* routine session-workflow.ts's `/session resume` calls) so the two seams
|
|
60
|
+
* cannot diverge on restoreTurnAnchors, resetAll-before-fromJSON, model
|
|
61
|
+
* reselection, or the modal-redirect panel-reopen skip — see
|
|
62
|
+
* core/session-resume-core.ts's header doc for the full list of divergences
|
|
63
|
+
* this closes. Everything below the resumeSessionCore call is plumbing only
|
|
64
|
+
* this seam performs (hook fire, session-spine mirror, shared-broker
|
|
65
|
+
* reopen, last-session pointer) plus this seam's own log/render idiom.
|
|
66
|
+
*/
|
|
67
|
+
export function createResumeSessionHandler(options: ResumeSessionOptions): (sessionId: string) => Promise<void> {
|
|
68
|
+
return async (sessionId: string): Promise<void> => {
|
|
43
69
|
try {
|
|
44
|
-
|
|
70
|
+
// Multi-instance safety, matching what `/session resume` already does
|
|
71
|
+
// for the text path: never fork a session another terminal is holding
|
|
72
|
+
// open without the operator saying so.
|
|
73
|
+
if (options.confirmLiveResume && !(await options.confirmLiveResume(sessionId))) {
|
|
74
|
+
options.conversation.log('Resume cancelled — the session is still open in another terminal.', { fg: '244' });
|
|
75
|
+
options.requestRender();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Pre-read purely for the emitSessionResumed announcement's turnCount,
|
|
79
|
+
// which reports the raw saved-snapshot size — resumeSessionCore performs
|
|
80
|
+
// its own load() right after this (SessionManager.load is a cheap JSONL
|
|
81
|
+
// parse; the tiny duplicate read keeps this announcement's meaning
|
|
82
|
+
// unchanged rather than repurposing it to a post-replay count).
|
|
83
|
+
const { messages: rawMessages } = options.sessionManager.load(sessionId);
|
|
45
84
|
emitSessionResumed(options.runtimeBus, {
|
|
46
85
|
sessionId: options.runtime.sessionId,
|
|
47
86
|
traceId: `${options.runtime.sessionId}:session-resume:${sessionId}`,
|
|
48
87
|
source: 'bootstrap',
|
|
49
88
|
}, {
|
|
50
89
|
sessionId,
|
|
51
|
-
turnCount:
|
|
52
|
-
});
|
|
53
|
-
options.conversation.fromJSON({
|
|
54
|
-
messages: messages as Parameters<typeof options.conversation.fromJSON>[0]['messages'],
|
|
55
|
-
title: meta.title,
|
|
56
|
-
titleSource: meta.titleSource,
|
|
90
|
+
turnCount: rawMessages.length,
|
|
57
91
|
});
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
sessionId,
|
|
62
|
-
snapshotTimestamp: meta.timestamp ?? 0,
|
|
92
|
+
|
|
93
|
+
const outcome = await resumeSessionCore(sessionId, {
|
|
94
|
+
sessionManager: options.sessionManager,
|
|
63
95
|
conversation: options.conversation,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
timestamp: Date.now(),
|
|
70
|
-
titleSource: meta.titleSource,
|
|
71
|
-
returnContext: meta.returnContext,
|
|
72
|
-
});
|
|
73
|
-
},
|
|
96
|
+
runtime: options.runtime,
|
|
97
|
+
surface: options.surface,
|
|
98
|
+
panelManager: options.panelManager,
|
|
99
|
+
selectModel: options.selectModel,
|
|
100
|
+
hydrateSessionUsage: options.hydrateSessionUsage,
|
|
74
101
|
});
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
options.hydrateSessionUsage?.();
|
|
102
|
+
const { meta, panels } = outcome;
|
|
103
|
+
|
|
78
104
|
options.onSessionIdChanged?.(sessionId);
|
|
79
|
-
if (meta?.model) options.runtime.model = meta.model;
|
|
80
|
-
if (meta?.provider) options.runtime.provider = meta.provider;
|
|
81
105
|
options.writeLastSessionPointer(sessionId);
|
|
82
106
|
void options.sharedSessionBroker.reopenSession(sessionId).catch((err) => { logger.debug('session broker reopen session failed', { err }); });
|
|
83
107
|
// Fire-and-forget spine mirror (reopen:true — the user resume verb).
|
|
84
108
|
options.sessionSpine.reopen({ sessionId, project: options.project, title: options.conversation.title || meta.title });
|
|
85
109
|
options.conversation.log(`Resumed session: ${sessionId}`, { fg: '135' });
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
reopenedPanels.push(panelId);
|
|
92
|
-
} catch {
|
|
93
|
-
// Ignore unavailable panels during restore.
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
if (reopenedPanels.length > 0) options.panelManager.show();
|
|
110
|
+
if (panels.movedToModal.length > 0) {
|
|
111
|
+
options.conversation.log(`Resume: ${panels.movedToModal.join(', ')} moved to a modal — reopen via its command instead of as a panel.`, { fg: '244' });
|
|
112
|
+
}
|
|
113
|
+
if (panels.notReopened.length > 0) {
|
|
114
|
+
options.conversation.log(`Resume: …and ${panels.notReopened.length} more not reopened (/panels to open)`, { fg: '244' });
|
|
97
115
|
}
|
|
98
116
|
const returnContextMode = getReturnContextMode(options.configManager);
|
|
99
117
|
if (returnContextMode !== 'off' && meta.returnContext) {
|
|
100
118
|
for (const line of formatReturnContextForDisplay(meta.returnContext)) {
|
|
101
119
|
options.conversation.log(`Resume: ${line}`, { fg: '244' });
|
|
102
120
|
}
|
|
103
|
-
if (
|
|
104
|
-
options.conversation.log(`Resume: Reopened panels: ${
|
|
121
|
+
if (panels.reopened.length > 0) {
|
|
122
|
+
options.conversation.log(`Resume: Reopened panels: ${panels.reopened.join(', ')}`, { fg: '244' });
|
|
105
123
|
}
|
|
106
124
|
if ((meta.returnContext.remoteRunners?.length ?? 0) > 0) {
|
|
107
125
|
options.conversation.log(`Resume: Remote re-entry -> /remote recover ${meta.returnContext.remoteRunners![0]}`, { fg: '244' });
|