mixdog 0.9.85 → 0.9.87

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.
Files changed (102) hide show
  1. package/README.md +7 -1
  2. package/package.json +4 -3
  3. package/src/defaults/agents.json +12 -0
  4. package/src/defaults/skills/setup/SKILL.md +44 -12
  5. package/src/help.mjs +40 -9
  6. package/src/lib/keychain-cjs.cjs +11 -1
  7. package/src/rules/agent/43-title-agent.md +22 -0
  8. package/src/rules/shared/01-tool.md +3 -3
  9. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +4 -2
  10. package/src/runtime/agent/orchestrator/mcp/client.mjs +24 -0
  11. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +84 -3
  12. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +125 -16
  13. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +2 -2
  14. package/src/runtime/agent/orchestrator/session/context-utils.mjs +73 -45
  15. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +15 -18
  16. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +1 -1
  17. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -0
  18. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +135 -9
  19. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +31 -12
  20. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +4 -0
  21. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +14 -0
  22. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +170 -0
  23. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +45 -6
  24. package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
  25. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +26 -0
  26. package/src/runtime/agent/orchestrator/session/token-bpe.mjs +42 -0
  27. package/src/runtime/agent/orchestrator/session/token-native.mjs +186 -0
  28. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +15 -5
  29. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +20 -0
  30. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -2
  31. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +24 -0
  32. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +8 -0
  33. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  34. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +286 -0
  35. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  36. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -3
  37. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +152 -18
  38. package/src/runtime/agent/orchestrator/tools/token-binary-fetcher.mjs +201 -0
  39. package/src/runtime/agent/orchestrator/tools/token-manifest.json +26 -0
  40. package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +3 -3
  41. package/src/runtime/media/adapters/gemini-video.mjs +1 -1
  42. package/src/runtime/media/renditions.mjs +1 -1
  43. package/src/runtime/media/store.mjs +1 -1
  44. package/src/runtime/memory/lib/memory-action-handlers.mjs +4 -1
  45. package/src/runtime/memory/tool-defs.mjs +19 -3
  46. package/src/runtime/shared/atomic-file.mjs +25 -14
  47. package/src/runtime/shared/automation-attachments.mjs +3 -3
  48. package/src/runtime/shared/child-guardian.mjs +15 -5
  49. package/src/runtime/shared/child-spawn-gate.mjs +6 -12
  50. package/src/runtime/shared/resource-admission.mjs +7 -3
  51. package/src/runtime/shared/turn-snapshot.mjs +0 -3
  52. package/src/session-runtime/lifecycle-api.mjs +49 -9
  53. package/src/session-runtime/prewarm.mjs +35 -0
  54. package/src/session-runtime/provider-auth-api.mjs +8 -0
  55. package/src/session-runtime/provider-usage.mjs +30 -5
  56. package/src/session-runtime/remote-control.mjs +8 -47
  57. package/src/session-runtime/remote-transcript.mjs +3 -7
  58. package/src/session-runtime/route-preparation.mjs +24 -0
  59. package/src/session-runtime/runtime-core.mjs +286 -548
  60. package/src/session-runtime/session-lifecycle.mjs +378 -0
  61. package/src/session-runtime/session-turn-api.mjs +9 -2
  62. package/src/session-runtime/workflow-agents-api.mjs +3 -0
  63. package/src/standalone/agent-shard/shard-child.mjs +300 -0
  64. package/src/standalone/agent-shard/shard-pool.mjs +443 -0
  65. package/src/standalone/agent-tool/job-views.mjs +329 -0
  66. package/src/standalone/agent-tool/spawn-flow.mjs +629 -0
  67. package/src/standalone/agent-tool/tag-registry.mjs +340 -0
  68. package/src/standalone/agent-tool.mjs +112 -941
  69. package/src/standalone/channel-daemon-transport.mjs +126 -185
  70. package/src/standalone/channel-daemon.mjs +9 -10
  71. package/src/standalone/usage-dashboard.mjs +23 -1
  72. package/src/tui/App.jsx +362 -2569
  73. package/src/tui/app/app-view.jsx +504 -0
  74. package/src/tui/app/channel-pickers.mjs +3 -8
  75. package/src/tui/app/create-app-pickers.mjs +313 -0
  76. package/src/tui/app/prompt-submit.mjs +501 -0
  77. package/src/tui/app/route-pickers.mjs +117 -152
  78. package/src/tui/app/settings-picker.mjs +90 -89
  79. package/src/tui/app/shell-layout.mjs +563 -0
  80. package/src/tui/app/slash-commands.mjs +7 -7
  81. package/src/tui/app/slash-dispatch.mjs +0 -45
  82. package/src/tui/app/usage-context-panels.mjs +288 -0
  83. package/src/tui/app/use-copy-selection.mjs +56 -0
  84. package/src/tui/app/use-global-key-input.mjs +151 -0
  85. package/src/tui/app/use-pasted-buffers.mjs +115 -0
  86. package/src/tui/app/use-prompt-draft-flow.mjs +167 -0
  87. package/src/tui/app/use-prompt-hint.mjs +58 -0
  88. package/src/tui/app/use-prompt-queue-history.mjs +100 -0
  89. package/src/tui/app/use-terminal-chrome.mjs +72 -0
  90. package/src/tui/app/use-transcript-activity.mjs +131 -0
  91. package/src/tui/app/use-welcome-prompt-hint.mjs +96 -0
  92. package/src/tui/components/StatusLine.jsx +1 -1
  93. package/src/tui/components/tool-execution/ResultBody.jsx +1 -1
  94. package/src/tui/dist/index.mjs +9857 -9159
  95. package/src/tui/engine/session-api-ext.mjs +47 -9
  96. package/src/tui/engine.mjs +7 -1
  97. package/src/tui/figures.mjs +0 -1
  98. package/src/ui/statusline-format.mjs +0 -1
  99. package/src/hooks/lib/permission-evaluator.cjs +0 -24
  100. package/src/lib/config-cjs.cjs +0 -61
  101. package/src/runtime/shared/launcher-control.mjs +0 -258
  102. package/src/runtime/shared/workspace-router.mjs +0 -259
@@ -0,0 +1,563 @@
1
+ // Shell layout derivation, extracted from App.jsx. Pure render-phase math:
2
+ // given the terminal size and every surface that reserves rows (panels,
3
+ // prompt box, queued band, welcome banner, hints), compute the transcript
4
+ // viewport height and the whole bottom-cluster row budget. Runs once per
5
+ // render; the few ref writes mirror the previous inline behavior.
6
+ import { isLiveSpinnerMetaVisible } from './live-spinner-visibility.mjs';
7
+ import { isCompletedTranscriptTailAppendedThisCommit } from './live-spinner-visibility.mjs';
8
+ import { promptContentRows, wrappedDetailRows, queuedBandRows } from './text-layout.mjs';
9
+
10
+ export function computeShellLayout({
11
+ providerPrompt,
12
+ channelPrompt,
13
+ hookPrompt,
14
+ settingsPrompt,
15
+ panelTransitionEpoch,
16
+ panelInkMaskEpoch,
17
+ toolApproval,
18
+ picker,
19
+ contextPanel,
20
+ usagePanel,
21
+ slashPaletteOpen,
22
+ tuiReady,
23
+ state,
24
+ resizeState,
25
+ frameColumns,
26
+ promptHint,
27
+ promptHintTone,
28
+ textEntryLayoutRows,
29
+ onboardingActive,
30
+ conditionalWelcomePromptHint,
31
+ welcomePromptHintDismissed,
32
+ welcomePromptHintRef,
33
+ welcomePromptHintVisibleRef,
34
+ panelTransitionRef,
35
+ projectBootInputLatchRef,
36
+ promptLayoutValueRef,
37
+ promptContentColumns,
38
+ transcriptBottomSlackRowsRef,
39
+ transcriptViewportRef,
40
+ frameRowsRef,
41
+ promptBoxRectRef,
42
+ panelCloseInkMaskRowsRef,
43
+ CORE_MULTILINE_TEXT_ENTRY_KINDS,
44
+ panelSignatureFlags,
45
+ panelKindSignature,
46
+ isInstantPanelCloseTransition,
47
+ PANEL_LAYOUT_SIG,
48
+ }) {
49
+ // ── Transcript viewport height ──────────────────────────────────────────
50
+ // ROOT-CAUSE FIX: the transcript must live in a box with an EXPLICIT numeric
51
+ // height + overflow:hidden so ink's renderer actually clips off-screen rows
52
+ // (render-node-to-output.js → output.clip uses the box's computed height). An
53
+ // unbounded negative-margin column inside a flexGrow box let stale rows
54
+ // overprint newer ones across incremental redraws. We reserve the rows the
55
+ // bottom cluster needs and give the transcript everything above it.
56
+ //
57
+ // viewportHeight = rows
58
+ // − welcome header (empty transcript only)
59
+ // − live status (thinking / spinner / TurnDone)
60
+ // − queued prompts (marginTop 1 + N rows, only when queued)
61
+ // − prompt meta (spinner / transient message / queued)
62
+ // − input box (2 border + wrapped content)
63
+ // − statusline (reserved L1 + L2 + outer gap; total 3 rows)
64
+ //
65
+ // Every sibling outside the viewport must be accounted for here; otherwise
66
+ // the total tree height exceeds the terminal and the input box gets pushed.
67
+ const textEntryPrompt = providerPrompt || channelPrompt || hookPrompt || settingsPrompt;
68
+ const hasTextEntryPrompt = !!textEntryPrompt;
69
+ const hasFloatingPanel = !!(toolApproval || picker || contextPanel || usagePanel || slashPaletteOpen || hasTextEntryPrompt);
70
+ const expandedOptionPanel = !!(toolApproval || picker || contextPanel || usagePanel || hasTextEntryPrompt);
71
+ const panelTransitionForBoot = panelTransitionRef.current;
72
+ if (panelTransitionForBoot.signature.includes('picker:project') && !picker) {
73
+ projectBootInputLatchRef.current = true;
74
+ }
75
+ const bootSettling = !tuiReady && state.items.length === 0 && !hasFloatingPanel && !projectBootInputLatchRef.current;
76
+ // Project selection (initial-entry experience) keeps the welcome banner
77
+ // visible above the picker / path-entry prompt, unlike other floating panels.
78
+ const projectSelectionActive = picker?.kind === 'project'
79
+ || settingsPrompt?.kind === 'project-new'
80
+ || settingsPrompt?.kind === 'project-create-confirm'
81
+ || settingsPrompt?.kind === 'project-rename';
82
+ // Slash search floats above the normal prompt. Actual option panels own the
83
+ // prompt/status area, so they hide those rows and expand into that space.
84
+ const inputBoxHidden = expandedOptionPanel || bootSettling;
85
+ const liveSpinner = state.spinner?.active ? state.spinner : (state.commandStatus?.active ? state.commandStatus : null);
86
+ // Command-status spinner (auto-clear/compact/etc.) is NOT part of the
87
+ // spinner → TurnDone handoff: it typically starts while the transcript tail
88
+ // is already a done row (idle session), so the done-at-tail suppression
89
+ // below must never hide it — that read as a frozen UI during auto-clear.
90
+ const liveSpinnerIsCommand = !state.spinner?.active && !!state.commandStatus?.active;
91
+ const latestToast = state.toasts?.length ? state.toasts[state.toasts.length - 1] : null;
92
+ const toastHint = latestToast ? latestToast.text : '';
93
+ const progressHint = state.progressHint || null;
94
+ const inputHint = promptHint || toastHint || (progressHint?.text || '');
95
+ const inputHintTone = promptHint
96
+ ? promptHintTone
97
+ : (latestToast?.tone || progressHint?.tone || 'info');
98
+ const latestTranscriptItem = state.items[state.items.length - 1] || null;
99
+ // Bottom meta band ownership is LIVE-SPINNER ONLY. A finished turn's done row
100
+ // (turndone/statusdone) is a normal transcript item and flows into scrollback
101
+ // like anything else, so the area directly above the prompt is CLEAR when the
102
+ // user is idle. Earlier this row was pinned in the meta band until the next
103
+ // transcript item was appended (to dodge an autowrap overprint/bleed), which
104
+ // left the completed status row stuck above the prompt while the user typed or
105
+ // sat idle. That bleed is now fixed at the source by the tool-output width
106
+ // clamp, so the pin is no longer needed. Kept as a named null const so the
107
+ // downstream meta-band/hint logic collapses cleanly to the spinner-only path.
108
+ const latestDoneItem = null;
109
+ const SCROLL_HINT_ROWS = 0;
110
+ const LIVE_STATUS_ROWS = 0;
111
+ // The standalone prompt box is 2 border rows + the wrapped PromptInput body.
112
+ // The one-row scroll baseline gap ABOVE the prompt is owned by
113
+ // transcriptGuardRows, not the prompt box itself. That keeps the scroll
114
+ // reference at "textbox + 1" while the prompt/statusline bottom stays fixed.
115
+ //
116
+ // This must track the prompt draft's REAL wrapped height. Reserving a constant
117
+ // one-line prompt lets long/multiline input grow the bottom cluster after the
118
+ // transcript viewport has already claimed those rows, which makes transcript
119
+ // body text overprint the textbox or slash command window.
120
+ const currentPromptLayoutRows = promptContentRows(promptLayoutValueRef.current, promptContentColumns);
121
+ const promptInputRows = inputBoxHidden ? 0 : currentPromptLayoutRows;
122
+ const promptBoxRows = inputBoxHidden ? 0 : 2 + promptInputRows;
123
+ const STATUSLINE_ROWS = 3;
124
+ // Shared panel chrome math. Every floating panel follows the same vertical
125
+ // rhythm INSIDE its round border: title row, blank, description/hint row,
126
+ // blank, then content. That is 4 non-content rows; the round border adds 2
127
+ // more, so chrome reserves 6 rows total. Reserving the full chrome here (even
128
+ // for panels that omit the description) guarantees the bordered title can
129
+ // never be clipped off the top — content rows shrink first when the terminal
130
+ // is short, because the floating container clips from the top (flex-end).
131
+ const PANEL_MAX_VISIBLE = 8;
132
+ const PANEL_CHROME_ROWS = 6;
133
+ const PANEL_BASE_ROWS = PANEL_MAX_VISIBLE + PANEL_CHROME_ROWS;
134
+ const PICKER_CHROME_ROWS = PANEL_CHROME_ROWS;
135
+ // TextEntryPanel content is one prompt line (chrome + 1) for single-line
136
+ // prompts, or up to PANEL_MAX_VISIBLE wrapped rows for core memory add/edit.
137
+ // PLUS an optional wrapped detail block (blank spacer + N wrapped rows — e.g.
138
+ // the manual OAuth URL). The floating container clips from the TOP (flex-end +
139
+ // overflow hidden), so under-reserving here pushed the bordered title off
140
+ // the top of the panel. Width matches the panel interior: frame − 2 border
141
+ // − 2 paddingX, same wrap-ansi math ink uses for wrap="wrap".
142
+ const textEntryKind = String(textEntryPrompt?.kind || '');
143
+ const textEntryMultiline = CORE_MULTILINE_TEXT_ENTRY_KINDS.has(textEntryKind);
144
+ const textEntryDetailText = String(textEntryPrompt?.detail || '').trim();
145
+ const textEntryDetailRows = textEntryDetailText
146
+ ? 1 + wrappedDetailRows(textEntryDetailText, Math.max(1, frameColumns - 4))
147
+ : 0;
148
+ const textEntryContentRows = textEntryMultiline ? textEntryLayoutRows : 1;
149
+ const TEXT_ENTRY_ROWS = PANEL_CHROME_ROWS + textEntryContentRows + textEntryDetailRows;
150
+ const OPTION_PANEL_EXTRA_ROWS = expandedOptionPanel ? 3 : 0;
151
+ const queuedVisible = !hasFloatingPanel && !inputBoxHidden && state.queued?.length > 0;
152
+ // While the slash palette is open it owns the area above the prompt, so the
153
+ // live spinner/meta row is suppressed entirely — no reservation and no render.
154
+ // Normalize the spinner → TurnDone handoff by making them occupy the SAME
155
+ // two-row slot. Completion appends turndone and clears spinner in one commit,
156
+ // so a completed row replaces the spinner slot with no transient jump. A
157
+ // statusdone can be emitted mid-turn (for example after compaction), so it
158
+ // must not suppress the still-active spinner.
159
+ const promptMetaVisible = isLiveSpinnerMetaVisible({
160
+ inputBoxHidden,
161
+ slashPaletteOpen,
162
+ liveSpinner,
163
+ liveSpinnerIsCommand,
164
+ latestTranscriptItem,
165
+ });
166
+ const promptMetaRows = promptMetaVisible ? 2 : 0;
167
+ // Toast/error text without a live spinner uses the existing transcript guard
168
+ // row directly above the prompt. Do NOT reserve another row here: that made a
169
+ // transient hint add a visible newline/prompt jump whenever no spinner was
170
+ // active.
171
+ const overlayHintRequested = !inputBoxHidden && !hasFloatingPanel && !liveSpinner && !!inputHint && !queuedVisible;
172
+ const overlayHintRows = 0;
173
+ // QueuedCommands renders each queued command at its FULL wrapped height
174
+ // (same content width the promoted transcript user row wraps at), pinned
175
+ // above the prompt box with no extra top-margin row. Reserving the true
176
+ // height keeps promotion from re-expanding the text mid-flight ("row jump").
177
+ // If the whole queue would eat too much of the frame, fall back to the old
178
+ // compact 1-row-per-entry truncation so the input box never leaves screen.
179
+ const queuedFullRows = queuedVisible
180
+ ? state.queued.reduce(
181
+ (sum, item) => sum + queuedBandRows(String(item.displayText || item.text || ''), Math.max(1, frameColumns - 4)),
182
+ 0,
183
+ )
184
+ : 0;
185
+ const queuedRowBudget = Math.max(3, Math.floor(resizeState.rows / 3));
186
+ const queuedCompact = queuedFullRows > queuedRowBudget;
187
+ const queuedRows = queuedVisible ? (queuedCompact ? state.queued.length : queuedFullRows) : 0;
188
+ const INPUT_BOX_ROWS = promptBoxRows + promptMetaRows + overlayHintRows;
189
+ // Welcome banner visibility (computed here, after the prompt/panel row math
190
+ // it depends on). The slash palette must NOT unmount the banner on the empty
191
+ // transcript: dropping the 11-row banner the moment '/' opened the palette
192
+ // (and remounting it on Esc) read as the whole top of the screen jumping on
193
+ // command-palette entry. Keep the banner mounted through a slash session
194
+ // whenever the terminal is tall enough to hold banner + palette + prompt +
195
+ // statusline at once; short terminals still yield the banner so the palette
196
+ // chrome never clips (the floating container clips from the top).
197
+ const WELCOME_BANNER_ROWS = 11;
198
+ const SLASH_PALETTE_ROWS = PANEL_MAX_VISIBLE + PANEL_CHROME_ROWS;
199
+ const slashKeepsWelcomeBanner = slashPaletteOpen
200
+ && resizeState.rows >= WELCOME_BANNER_ROWS + SLASH_PALETTE_ROWS + INPUT_BOX_ROWS + STATUSLINE_ROWS + 1;
201
+ const showWelcomeBanner = (state.items.length === 0 && (!hasFloatingPanel || slashKeepsWelcomeBanner))
202
+ || projectSelectionActive || onboardingActive;
203
+ const WELCOME_ROWS = showWelcomeBanner ? WELCOME_BANNER_ROWS : 0;
204
+ const baseReserve = WELCOME_ROWS + SCROLL_HINT_ROWS + LIVE_STATUS_ROWS + INPUT_BOX_ROWS + STATUSLINE_ROWS + queuedRows;
205
+ const maxFloatingPanelRows = Math.max(0, resizeState.rows - baseReserve - 1);
206
+ const desiredFloatingPanelRows = toolApproval
207
+ ? PANEL_CHROME_ROWS + 2 + OPTION_PANEL_EXTRA_ROWS
208
+ : picker
209
+ ? (picker.fillAvailable ? maxFloatingPanelRows : PANEL_BASE_ROWS + OPTION_PANEL_EXTRA_ROWS)
210
+ : contextPanel
211
+ ? PANEL_BASE_ROWS + OPTION_PANEL_EXTRA_ROWS + 3
212
+ : usagePanel
213
+ ? PANEL_BASE_ROWS + OPTION_PANEL_EXTRA_ROWS
214
+ : slashPaletteOpen
215
+ ? PANEL_MAX_VISIBLE + PANEL_CHROME_ROWS
216
+ : hasTextEntryPrompt
217
+ ? TEXT_ENTRY_ROWS
218
+ : 0;
219
+ const floatingPanelRows = desiredFloatingPanelRows > 0
220
+ ? Math.min(desiredFloatingPanelRows, maxFloatingPanelRows)
221
+ : 0;
222
+ // Give the list every content row the panel exposes. The panel already grew
223
+ // by OPTION_PANEL_EXTRA_ROWS; previously that growth was subtracted back out
224
+ // here, so the rows leaked into an empty flexGrow gap instead of the list.
225
+ // Reserving only PICKER_CHROME_ROWS lets the list occupy the full interior
226
+ // (the footer's own reservation is handled inside Picker).
227
+ const pickerVisibleRows = picker
228
+ ? Math.max(1, floatingPanelRows - PICKER_CHROME_ROWS)
229
+ : PANEL_MAX_VISIBLE;
230
+ const rawBottomReserve = baseReserve + floatingPanelRows;
231
+ const bottomClusterRows = INPUT_BOX_ROWS + STATUSLINE_ROWS + queuedRows + floatingPanelRows;
232
+ const panelLayoutSignature = [
233
+ toolApproval ? 'tool' : '',
234
+ picker ? `picker:${picker.kind || ''}:${picker.fillAvailable ? 'fill' : 'fit'}` : '',
235
+ contextPanel ? 'context' : '',
236
+ usagePanel ? 'usage' : '',
237
+ slashPaletteOpen ? 'slash' : '',
238
+ hasTextEntryPrompt ? `text:${textEntryPrompt?.kind || ''}` : '',
239
+ inputBoxHidden ? 'input-hidden' : 'input-visible',
240
+ floatingPanelRows,
241
+ promptBoxRows,
242
+ promptMetaRows,
243
+ queuedRows,
244
+ WELCOME_ROWS,
245
+ ].join('|');
246
+ const panelTransition = panelTransitionRef.current;
247
+ const panelLayoutChanged = Boolean(panelTransition.signature && panelTransition.signature !== panelLayoutSignature);
248
+ let panelTransitionClearRows = 0;
249
+ let panelTransitionGuardRows = 0;
250
+ if (panelLayoutChanged) {
251
+ const panelShrinkRows = Math.max(0, panelTransition.reserve - bottomClusterRows);
252
+ const initialProjectEntryClose = state.items.length === 0;
253
+ // Prompt-row-only churn (promptBoxRows/promptMetaRows/queuedRows shifting
254
+ // while no panel opened/closed/changed kind and floatingPanelRows itself is
255
+ // unchanged — see PANEL_KIND_TOKEN_COUNT) must not fall into the clearRows
256
+ // + setTimeout recommit path below: that inserts a full extra blank row for
257
+ // one commit, which IS the newline-add/remove jolt while typing. Route a
258
+ // shrink here through the same one-commit ink-mask path as an instant panel
259
+ // close so the stale row is masked in the very commit it disappears; growth
260
+ // already needs no clearance (panelShrinkRows is 0 in that case).
261
+ const promptRowsOnlyChange = panelShrinkRows > 0
262
+ && panelKindSignature(panelTransition.signature) === panelKindSignature(panelLayoutSignature);
263
+ // Turn-end spinner meta collapse: the 2-row live-spinner band disappears in
264
+ // the SAME commit the engine appends the turndone/statusdone tail (see
265
+ // engine.mjs runTurn — turndone + spinner:null land in one set()). That new
266
+ // done row already replaces the vacated height, so masking those rows blank
267
+ // for one commit only to clear them on the next commit IS the visible
268
+ // transcript bounce. Exempt exactly the meta-collapse rows from the ink mask
269
+ // when the done row is the transcript tail. Every other prompt-row-only
270
+ // shrink (typing newline removal, queued-row churn) AND the reclaimed/no-op
271
+ // path (engine.mjs skips turndone, so the completed-tail check stays false and no
272
+ // row replaces the height) keep the mask so they still reclaim smoothly.
273
+ const prevMetaRows = Number(String(panelTransition.signature).split('|')[PANEL_LAYOUT_SIG.PROMPT_META]) || 0;
274
+ const nextMetaRows = Number(String(panelLayoutSignature).split('|')[PANEL_LAYOUT_SIG.PROMPT_META]) || 0;
275
+ // Require the done row to have been appended in THIS commit (tail id changed
276
+ // since the last commit). A command spinner can leave a STALE done row at the
277
+ // tail and then clear without appending statusdone (e.g. /recall — see
278
+ // engine.mjs), collapsing the meta band with NO same-commit backfill; masking
279
+ // must stay on for that path or the vacated rows overpaint the stale row.
280
+ const doneTailAppendedThisCommit = isCompletedTranscriptTailAppendedThisCommit(
281
+ latestTranscriptItem,
282
+ panelTransition.tailId,
283
+ );
284
+ const spinnerMetaCollapseRows = doneTailAppendedThisCommit
285
+ ? Math.max(0, prevMetaRows - nextMetaRows)
286
+ : 0;
287
+ // Queued-band promotion: drain() removes the queued band and appends the
288
+ // promoted user transcript row in the SAME commit (session-flow.mjs drain
289
+ // → pushUserOrSyntheticItem → runTurn spinner, one microtask flush). The
290
+ // new user row (full wrapped height + margin) already backfills the
291
+ // vacated band rows, so masking them blank for one commit only to drop
292
+ // the mask on the next commit made the whole transcript bounce down.
293
+ // Exempt exactly the vacated queued rows when a user row landed at the
294
+ // tail in this commit; queue edits/removals without a tail append (tail
295
+ // id unchanged, or non-user tail) keep the mask.
296
+ const prevQueuedSigRows = Number(String(panelTransition.signature).split('|')[PANEL_LAYOUT_SIG.QUEUED]) || 0;
297
+ const nextQueuedSigRows = Number(String(panelLayoutSignature).split('|')[PANEL_LAYOUT_SIG.QUEUED]) || 0;
298
+ const userTailAppendedThisCommit = latestTranscriptItem?.kind === 'user'
299
+ && (latestTranscriptItem?.id ?? null) !== panelTransition.tailId;
300
+ const queuedPromoteCollapseRows = userTailAppendedThisCommit
301
+ ? Math.max(0, prevQueuedSigRows - nextQueuedSigRows)
302
+ : 0;
303
+ const instantPanelClose = panelShrinkRows > 0
304
+ && (promptRowsOnlyChange
305
+ || isInstantPanelCloseTransition(panelTransition.signature, panelLayoutSignature, initialProjectEntryClose));
306
+ // Slash palette opening on the empty welcome screen: bottomReserve already
307
+ // grows to its final size in this same commit (floatingPanelRows reflects
308
+ // slashPaletteOpen immediately, no clearRows needed), but the renderer can
309
+ // still overpaint the just-vacated transcript row for one commit. Borrow
310
+ // the transitional guard-row mechanism (normally used for tall panel
311
+ // closes below) for exactly one commit on the open transition itself —
312
+ // this only carves an extra blank row out of transcriptContentHeight, it
313
+ // does not touch bottomReserve/floatingPanelRows/palette height.
314
+ const slashOpenOnEmptyTranscript = initialProjectEntryClose
315
+ && !panelSignatureFlags(panelTransition.signature).slash
316
+ && panelSignatureFlags(panelLayoutSignature).slash;
317
+ if (instantPanelClose) {
318
+ // Slash palette and initial project-entry closes land on the final bottom
319
+ // reserve in one commit. Paint reclaimed rows as a blank mask band below
320
+ // the transcript clip instead of inflating bottomReserve + reclaiming on
321
+ // the next tick. Subtract any turn-end spinner-meta rows that the same-
322
+ // commit done tail already backfills (spinnerMetaCollapseRows) so that
323
+ // transition masks nothing and does not bounce; same for queued-band
324
+ // rows backfilled by a just-promoted user row (queuedPromoteCollapseRows).
325
+ panelCloseInkMaskRowsRef.current = Math.max(0, panelShrinkRows - spinnerMetaCollapseRows - queuedPromoteCollapseRows);
326
+ panelTransition.clearRows = 0;
327
+ panelTransition.guardRows = 0;
328
+ panelTransition.epoch = panelTransitionEpoch;
329
+ } else if (slashOpenOnEmptyTranscript) {
330
+ panelTransitionClearRows = 0;
331
+ panelTransitionGuardRows = 1;
332
+ panelTransition.clearRows = 0;
333
+ panelTransition.guardRows = 1;
334
+ panelTransition.epoch = panelTransitionEpoch;
335
+ } else {
336
+ // Tall panel closes must land on the final bottom reserve in the same
337
+ // commit. Inflating bottomReserve with temporary clearance makes the
338
+ // transcript/prompt area move once, then snap back on the timer commit.
339
+ // Instead, keep the reclaimed rows inside the fixed viewport as a blank
340
+ // one-frame mask, matching the instant-close path above.
341
+ panelCloseInkMaskRowsRef.current = panelShrinkRows;
342
+ panelTransition.clearRows = 0;
343
+ panelTransition.guardRows = 0;
344
+ panelTransition.epoch = panelTransitionEpoch;
345
+ }
346
+ } else if (panelTransition.epoch === panelTransitionEpoch) {
347
+ panelTransitionClearRows = panelTransition.clearRows || 0;
348
+ panelTransitionGuardRows = panelTransition.guardRows || 0;
349
+ }
350
+ if (desiredFloatingPanelRows > 0) {
351
+ panelCloseInkMaskRowsRef.current = 0;
352
+ }
353
+ void panelInkMaskEpoch;
354
+ const panelCloseInkMaskRows = desiredFloatingPanelRows > 0 ? 0 : panelCloseInkMaskRowsRef.current;
355
+ const bottomReserve = rawBottomReserve + panelTransitionClearRows;
356
+ const viewportHeight = Math.max(1, resizeState.rows - bottomReserve);
357
+ // Keep one physical row between the transcript clip and the bottom cluster
358
+ // even when pinned to the live tail. Windows Terminal/conhost can still
359
+ // surface one clipped/off-by-one transcript row below the statusline during
360
+ // rapid tool-card updates; a permanent guard row makes that row blank instead
361
+ // of a tool header/detail.
362
+ const guardCapacityRows = Math.max(0, viewportHeight - 1);
363
+ const baseGuardRows = guardCapacityRows > 0 ? 1 : 0;
364
+ // ── Scroll-time overprint guard ───────────────────────────────────────────
365
+ // Wheel/manual scroll pushes the transcript column DOWN via a negative
366
+ // marginBottom (see the viewport render). Under conhost/Windows Terminal the
367
+ // incremental redraw can leave the row that slid past the clip edge painted
368
+ // OVER the bottom cluster (input box / statusline) for a frame — the reported
369
+ // "scrolled text shows on the statusline row" bug. One guard row is enough
370
+ // while pinned to the live tail, but during an active scroll the slid row can
371
+ // still bleed one line further, so widen the guard to TWO rows whenever the
372
+ // viewport is genuinely scrolled up. The extra blank row absorbs the stray
373
+ // paint instead of the statusline. Requires a viewport tall enough to spare
374
+ // the row, and never shrinks below the base guard.
375
+ // Gate on the same follow-aware basis the transcript window uses for
376
+ // renderScrollOffset (see use-transcript-window.mjs targetNearBottom): the
377
+ // live `scrollOffset` state can still read >0 for one frame after a wheel
378
+ // turn re-arms bottom-follow, while this same frame already renders with
379
+ // renderScrollOffset=0. Without this, the stale offset shrinks
380
+ // transcriptContentHeight by one row that never actually gets rendered,
381
+ // producing a one-row bounce that snaps back once state catches up.
382
+ // [2026-07-06] Scroll-time extra guard DISABLED: the widened (2-row) guard
383
+ // rendered as a visibly empty band between the transcript and the prompt box
384
+ // whenever the viewport was scrolled up (user-reported "bottom rows look
385
+ // blank while scrolling"). The base 1-row guard below stays; if the
386
+ // scrolled-row-over-statusline overpaint resurfaces, fix it in the renderer
387
+ // diff (clip/erase) instead of carving more blank viewport rows.
388
+ const scrollGuardRows = 0;
389
+ const transcriptGuardRows = Math.min(guardCapacityRows, baseGuardRows + panelTransitionGuardRows + scrollGuardRows);
390
+ // Welcome prompt hint: a one-row band rendered INSIDE the transcript
391
+ // viewport (as a sibling below the content clip), so it must be part of the
392
+ // viewport row accounting computed right below. Left unaccounted, the slash
393
+ // palette close commit on the empty welcome screen painted one extra
394
+ // physical row (hint reappears in the same commit as the close mask), so
395
+ // the prompt box + statusline dipped one row and snapped back on the
396
+ // mask-clear commit.
397
+ const welcomePromptHintText = conditionalWelcomePromptHint || welcomePromptHintRef.current || '';
398
+ const welcomePromptHintVisible = Boolean(
399
+ welcomePromptHintText
400
+ && !welcomePromptHintDismissed
401
+ && state.items.length === 0
402
+ && !hasFloatingPanel
403
+ && !inputBoxHidden
404
+ && !queuedVisible
405
+ && !liveSpinner
406
+ && !inputHint
407
+ );
408
+ // Tiny terminals: guard rows can already consume all but one viewport row
409
+ // (guardCapacityRows = viewportHeight - 1). transcriptContentHeight clamps
410
+ // to >= 1, so an unconditional hint row would paint viewportHeight + 1 rows
411
+ // and push the prompt/statusline down. The hint yields unless at least one
412
+ // content row remains beside it.
413
+ const welcomePromptHintRows = welcomePromptHintVisible
414
+ && (viewportHeight - transcriptGuardRows) >= 2 ? 1 : 0;
415
+ welcomePromptHintVisibleRef.current = welcomePromptHintRows > 0;
416
+ // Transient hint/error on the EMPTY transcript: the guard row sits directly
417
+ // above the prompt box, so painting the hint there hugs the textbox one row
418
+ // below where the live-spinner line renders. Carve one in-viewport row ABOVE
419
+ // the guard row instead so the hint's baseline matches the spinner row (two
420
+ // rows above the box, guard row stays blank as the spacer). This is an
421
+ // in-viewport carve like welcomePromptHintRows — bottomReserve is untouched,
422
+ // so the prompt box and statusline never move. Non-empty transcripts keep the
423
+ // existing attach-to-last-item / guard-row fallback placements.
424
+ const overlayHintBandRows = overlayHintRequested
425
+ && state.items.length === 0
426
+ && (viewportHeight - transcriptGuardRows - welcomePromptHintRows) >= 2 ? 1 : 0;
427
+ // Instant panel close (slash palette): the reclaimed rows stay blank for
428
+ // exactly one commit via panelCloseMaskRows. The mask MUST be part of this
429
+ // frame's row accounting — subtract it from the transcript content height
430
+ // and render it as a sibling band below the content clip (where the closed
431
+ // panel's ink was). Rendering it inside the scrolled transcript column made
432
+ // the painted column taller than the accounted viewport for one frame, so
433
+ // the prompt/statusline dropped a row and snapped back on the mask-clear
434
+ // commit (the "textbox dips when the slash palette closes" bug).
435
+ const panelCloseMaskRows = Math.min(
436
+ panelCloseInkMaskRows,
437
+ Math.max(0, viewportHeight - transcriptGuardRows - welcomePromptHintRows - overlayHintBandRows - 1),
438
+ );
439
+ const transcriptContentHeight = Math.max(
440
+ 1,
441
+ viewportHeight - transcriptGuardRows - panelCloseMaskRows - welcomePromptHintRows - overlayHintBandRows,
442
+ );
443
+ // Keep the keyboard-selection edge step anchored to the base guard. This is
444
+ // not a follow threshold: any positive wheel target is a reading position;
445
+ // only the true bottom may auto-follow a live tail.
446
+ const transcriptBottomSlackRows = Math.max(0, baseGuardRows);
447
+ transcriptBottomSlackRowsRef.current = transcriptBottomSlackRows;
448
+ transcriptViewportRef.current = {
449
+ top: WELCOME_ROWS,
450
+ bottom: Math.max(WELCOME_ROWS, WELCOME_ROWS + transcriptContentHeight - 1),
451
+ };
452
+ // [mixdog] Keep the live terminal row count current for the mouse handler's
453
+ // region routing + status-band selection clip (see onData).
454
+ frameRowsRef.current = Math.max(1, Number(resizeState.rows) || 24);
455
+ // When the prompt box is hidden (floating panel / option panel owns the
456
+ // bottom area), drop its stale measured rect so the mouse handler does not
457
+ // route presses to a prompt box that is not on screen.
458
+ if (inputBoxHidden) promptBoxRectRef.current = null;
459
+ // Toast/error text has two mutually exclusive placements:
460
+ // - while a live status row exists (thinking/compacting/responding), attach it
461
+ // to that row so the bottom cluster reserves exactly one status band;
462
+ // - otherwise render it into the normal one-row gap above the prompt, replacing
463
+ // the blank spacer instead of reserving an extra row. This keeps late errors
464
+ // from pushing the prompt/statusline upward, and when thinking starts the
465
+ // hint moves into the live row on the same render instead of double-painting.
466
+ // Transient hint placement while no spinner owns the band is resolved after
467
+ // transcript windowing (see overlayHintOnLastItem / overlayHintFallbackRow).
468
+ const spinnerHintWidth = inputHint
469
+ ? Math.max(1, Math.min(Math.max(1, frameColumns - 4), Math.max(12, Math.floor(frameColumns * 0.42))))
470
+ : 0;
471
+ // When no live spinner owns a status band, the transient hint/error is drawn
472
+ // into the existing transcript guard row directly above the prompt. Mirror the
473
+ // spinner-row placement: a fixed-width right slot, not a full-width left box.
474
+ const guardHintWidth = inputHint
475
+ ? Math.max(1, Math.min(Math.max(1, frameColumns - 4), Math.max(12, Math.floor(frameColumns * 0.42))))
476
+ : 0;
477
+ const transientStatusWidth = liveSpinner ? spinnerHintWidth : guardHintWidth;
478
+ const promptSpinnerColumns = liveSpinner && inputHint
479
+ ? Math.max(1, frameColumns - spinnerHintWidth - 1)
480
+ : frameColumns;
481
+
482
+ return {
483
+ textEntryPrompt,
484
+ hasTextEntryPrompt,
485
+ hasFloatingPanel,
486
+ expandedOptionPanel,
487
+ panelTransitionForBoot,
488
+ bootSettling,
489
+ projectSelectionActive,
490
+ inputBoxHidden,
491
+ liveSpinner,
492
+ liveSpinnerIsCommand,
493
+ latestToast,
494
+ toastHint,
495
+ progressHint,
496
+ inputHint,
497
+ inputHintTone,
498
+ latestTranscriptItem,
499
+ latestDoneItem,
500
+ SCROLL_HINT_ROWS,
501
+ LIVE_STATUS_ROWS,
502
+ currentPromptLayoutRows,
503
+ promptInputRows,
504
+ promptBoxRows,
505
+ STATUSLINE_ROWS,
506
+ PANEL_MAX_VISIBLE,
507
+ PANEL_CHROME_ROWS,
508
+ PANEL_BASE_ROWS,
509
+ PICKER_CHROME_ROWS,
510
+ textEntryKind,
511
+ textEntryMultiline,
512
+ textEntryDetailText,
513
+ textEntryDetailRows,
514
+ textEntryContentRows,
515
+ TEXT_ENTRY_ROWS,
516
+ OPTION_PANEL_EXTRA_ROWS,
517
+ queuedVisible,
518
+ promptMetaVisible,
519
+ promptMetaRows,
520
+ overlayHintRequested,
521
+ overlayHintRows,
522
+ queuedFullRows,
523
+ queuedRowBudget,
524
+ queuedCompact,
525
+ queuedRows,
526
+ INPUT_BOX_ROWS,
527
+ WELCOME_BANNER_ROWS,
528
+ SLASH_PALETTE_ROWS,
529
+ slashKeepsWelcomeBanner,
530
+ showWelcomeBanner,
531
+ WELCOME_ROWS,
532
+ baseReserve,
533
+ maxFloatingPanelRows,
534
+ desiredFloatingPanelRows,
535
+ floatingPanelRows,
536
+ pickerVisibleRows,
537
+ rawBottomReserve,
538
+ bottomClusterRows,
539
+ panelLayoutSignature,
540
+ panelTransition,
541
+ panelLayoutChanged,
542
+ panelTransitionClearRows,
543
+ panelTransitionGuardRows,
544
+ panelCloseInkMaskRows,
545
+ bottomReserve,
546
+ viewportHeight,
547
+ guardCapacityRows,
548
+ baseGuardRows,
549
+ scrollGuardRows,
550
+ transcriptGuardRows,
551
+ welcomePromptHintText,
552
+ welcomePromptHintVisible,
553
+ welcomePromptHintRows,
554
+ overlayHintBandRows,
555
+ panelCloseMaskRows,
556
+ transcriptContentHeight,
557
+ transcriptBottomSlackRows,
558
+ spinnerHintWidth,
559
+ guardHintWidth,
560
+ transientStatusWidth,
561
+ promptSpinnerColumns,
562
+ };
563
+ }
@@ -5,23 +5,23 @@
5
5
  */
6
6
  export const SLASH_COMMANDS = [
7
7
  { name: 'clear', usage: '/clear', aliases: ['new'], aliasUsage: ['new'], description: 'Start a fresh chat' },
8
- { name: 'project', usage: '/project', aliases: ['projects'], aliasUsage: ['projects'], showAliasUsage: false, description: 'Switch working directory (project)' },
8
+ { name: 'project', usage: '/project', aliases: ['projects'], aliasUsage: ['projects'], showAliasUsage: false, params: '[path]', description: 'Switch working directory (project)' },
9
9
  { name: 'compact', usage: '/compact', description: 'Compact older conversation context' },
10
- { name: 'autoclear', usage: '/autoclear', description: 'Reduce cache-miss cost after long idle gaps' },
11
- { name: 'resume', usage: '/resume', description: 'Resume a saved chat' },
10
+ { name: 'autoclear', usage: '/autoclear', params: '[on|off|duration]', description: 'Reduce cache-miss cost after long idle gaps' },
11
+ { name: 'resume', usage: '/resume', params: '[id]', description: 'Resume a saved chat' },
12
12
  { name: 'context', usage: '/context', description: 'Show current context surface' },
13
13
  { name: 'usage', usage: '/usage', params: '[refresh]', description: 'Show total provider quota / balance' },
14
- { name: 'model', usage: '/model', description: 'Switch model for subsequent turns' },
14
+ { name: 'model', usage: '/model', params: '[name|refresh]', description: 'Switch model for subsequent turns' },
15
15
  { name: 'search', usage: '/search', description: 'Set the web search provider/model' },
16
- { name: 'workflow', usage: '/workflow', description: 'Switch the active workflow' },
16
+ { name: 'workflow', usage: '/workflow', params: '[name]', description: 'Switch the active workflow' },
17
17
  { name: 'outputstyle', usage: '/OutputStyle', aliases: ['output-style', 'style'], aliasUsage: ['style'], showAliasUsage: false, params: '[name]', description: 'Switch Lead output style' },
18
18
  { name: 'theme', usage: '/theme', params: '[id]', description: 'Change the TUI color theme' },
19
- { name: 'agents', usage: '/agents', description: 'Show available workflow agents' },
19
+ { name: 'agents', usage: '/agents', params: '[refresh]', description: 'Show available workflow agents' },
20
20
  { name: 'effort', usage: '/effort', params: '[level]', description: 'Set reasoning effort for the current model' },
21
21
  { name: 'fast', usage: '/fast', params: '[on|off]', description: 'Toggle Fast mode for the current model' },
22
22
  { name: 'mcp', usage: '/mcp', description: 'Manage MCP servers and tools' },
23
23
  { name: 'skills', usage: '/skills', description: 'Choose a skill for the next request' },
24
- { name: 'memory', usage: '/memory', description: 'List and edit core memories' },
24
+ { name: 'memory', usage: '/memory', params: '[status|core ...]', description: 'List and edit core memories' },
25
25
  { name: 'plugins', usage: '/plugins', description: 'Manage local plugin integrations' },
26
26
  { name: 'hooks', usage: '/hooks', description: 'Manage before-tool hook rules and events' },
27
27
  { name: 'providers', usage: '/providers', description: 'Manage auth, API keys, OAuth, and local endpoints' },