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,504 @@
1
+ // App view tree, extracted from App.jsx. Pure JSX assembly: the prompt
2
+ // input control and the full shell layout (welcome banner, transcript
3
+ // viewport, floating panels, prompt cluster, statusline). All data and
4
+ // handlers arrive via one ctx object from App().
5
+ import React from 'react';
6
+ import { isCompletedTranscriptTailAppendedThisCommit } from './live-spinner-visibility.mjs';
7
+ import { Box, Text } from 'ink';
8
+ import { theme, surfaceBackground } from '../theme.mjs';
9
+ import { centerLine, promptStatusColor, toolApprovalDescription } from './app-format.mjs';
10
+ import { TRANSCRIPT_WINDOW_TAIL_OVERSCAN_ROWS } from './transcript-window.mjs';
11
+ import { localPackageVersion } from '../../runtime/shared/update-checker.mjs';
12
+ import { Spinner } from '../components/Spinner.jsx';
13
+ import { StatusLine } from '../components/StatusLine.jsx';
14
+ import { PromptInput } from '../components/PromptInput.jsx';
15
+ import { QueuedCommands } from '../components/QueuedCommands.jsx';
16
+ import { Picker } from '../components/Picker.jsx';
17
+ import { SlashCommandPalette } from '../components/SlashCommandPalette.jsx';
18
+ import { ContextPanel } from '../components/ContextPanel.jsx';
19
+ import { UsagePanel } from '../components/UsagePanel.jsx';
20
+ import { TextEntryPanel } from '../components/TextEntryPanel.jsx';
21
+ import { Item } from '../components/TranscriptItem.jsx';
22
+ export function renderAppView(ctx) {
23
+ const { PANEL_MAX_VISIBLE, acceptSlashPalette, activeSlashQuery, activeTools, agentRevision, cancelChannelPrompt, cancelHookPrompt, cancelProviderPrompt, cancelSettingsPrompt, cancelSlashPalette, channelPrompt, clearPromptHint, completeSlashPalette, contextPanel, cycleWorkflowFromPrompt, exiting, expandedOptionPanel, floatingPanelRows, frameColumns, gridSelectionActiveRef, guardHintWidth, handlePromptEscape, handlePromptHistoryNavigate, handlePromptInterrupt, handlePromptPaste, hookPrompt, initialStatusLine, inputBoxHidden, inputHint, inputHintTone, liveSpinner, onPromptDraftChange, onSubmit, overlayHintAttachItemIndex, overlayHintBandRows, overlayHintFallbackRow, overlayHintOnLastItem, panelCloseMaskRows, panelInkMaskEpoch, panelTransitionClearRows, panelTransitionEpoch, picker, pickerOpenedFromEnterRef, pickerOpenedFromEnterTimerRef, pickerVisibleRows, promptBoxRectRef, promptBoxRows, promptDraft, promptDraftOverride, promptMetaVisible, promptMouseSelectionRef, promptSelectionRef, promptSpinnerColumns, promptValueRef, providerPrompt, queuedCompact, queuedVisible, renderedTranscriptItems, resizeEpoch, resizeState, restoreQueuedToPrompt, setPicker, setSlashIndex, setTextEntryLayoutRows, settingsPrompt, showWelcomeBanner, slashCommands, slashIndex, slashPaletteOpen, state, statuslineStats, store, toolApproval, toolOutputExpanded, transcriptContentHeight, transcriptGuardRows, transcriptMeasureRef, transcriptTailPinned, transcriptWindow, transientStatusWidth, tuiReady, usagePanel, viewportHeight, welcomePromptHintRows, welcomePromptHintText } = ctx; /* DESTRUCTURE */
24
+ const promptInputControl = (
25
+ <PromptInput
26
+ onSubmit={onSubmit}
27
+ disabled={exiting || !!picker || !!toolApproval || !tuiReady}
28
+ onDraftChange={onPromptDraftChange}
29
+ interruptActive={state.busy}
30
+ onInterrupt={handlePromptInterrupt}
31
+ initialValue={promptDraft}
32
+ draftOverride={promptDraftOverride}
33
+ valueRef={promptValueRef}
34
+ selectionRef={promptSelectionRef}
35
+ boxRectRef={promptBoxRectRef}
36
+ mouseSelectionRef={promptMouseSelectionRef}
37
+ suppressShiftNavRef={gridSelectionActiveRef}
38
+ hint=""
39
+ hintTone={inputHintTone}
40
+ mask={false}
41
+ onEscape={handlePromptEscape}
42
+ onTab={cycleWorkflowFromPrompt}
43
+ onPasteText={handlePromptPaste}
44
+ onHistoryNavigate={handlePromptHistoryNavigate}
45
+ // Palette stays MOUNTED with 0 matches (stable height, no flicker), but
46
+ // key capture (Enter/arrows/Esc routing) only engages when a command can
47
+ // actually be accepted — otherwise Enter must submit the raw text as
48
+ // before instead of dead-ending in the palette accept path.
49
+ commandPaletteActive={slashPaletteOpen && slashCommands.length > 0}
50
+ onCommandPaletteNavigate={(direction) => {
51
+ setSlashIndex((index) => {
52
+ const total = slashCommands.length;
53
+ if (total === 0) return 0;
54
+ if (direction === 'home') return 0;
55
+ if (direction === 'end') return total - 1;
56
+ const step = direction === 'left'
57
+ ? -1
58
+ : direction === 'right'
59
+ ? 1
60
+ : Number(direction) || 0;
61
+ if (step === 1 || step === -1) return (index + step + total) % total;
62
+ return Math.max(0, Math.min(total - 1, index + step));
63
+ });
64
+ }}
65
+ onCommandPaletteAccept={acceptSlashPalette}
66
+ onCommandPaletteCancel={cancelSlashPalette}
67
+ onCommandPaletteComplete={completeSlashPalette}
68
+ onRestoreQueued={(currentText) => restoreQueuedToPrompt({ restoreDraft: true, showHint: false, currentText })}
69
+ />
70
+ );
71
+
72
+ return (
73
+ // Fullscreen layout: a full-height column (height = terminal rows) pins the
74
+ // input cluster + statusline to the physical bottom (flexShrink={0}), while
75
+ // the transcript fills the space above and is bottom-aligned so messages
76
+ // stack up from just over the input. A top flexGrow spacer sinks the whole
77
+ // stack to the bottom; the transcript itself is a fixed-height clipping
78
+ // viewport (see viewportHeight above).
79
+ <Box flexDirection="column" width={frameColumns} height={resizeState.rows} backgroundColor={surfaceBackground()}>
80
+ {/* Empty-transcript header stays outside the bottom-anchored viewport and
81
+ has its own reserved rows, so it cannot steal space from the input. */}
82
+ {showWelcomeBanner ? (
83
+ <Box flexDirection="column" height={7} flexShrink={0} marginTop={3} marginBottom={1} backgroundColor={surfaceBackground()}>
84
+ <Text color={theme.text} bold>{centerLine('███╗ ███╗██╗██╗ ██╗██████╗ ██████╗ ██████╗ ', frameColumns)}</Text>
85
+ <Text color={theme.text} bold>{centerLine('████╗ ████║██║╚██╗██╔╝██╔══██╗██╔═══██╗██╔════╝ ', frameColumns)}</Text>
86
+ <Text color={theme.logo ?? theme.claude} bold>{centerLine('██╔████╔██║██║ ╚███╔╝ ██║ ██║██║ ██║██║ ███╗', frameColumns)}</Text>
87
+ <Text color={theme.logo ?? theme.claude} bold>{centerLine('██║╚██╔╝██║██║ ██╔██╗ ██║ ██║██║ ██║██║ ██║', frameColumns)}</Text>
88
+ <Text color={theme.logo ?? theme.claude} bold>{centerLine('██║ ╚═╝ ██║██║██╔╝ ██╗██████╔╝╚██████╔╝╚██████╔╝', frameColumns)}</Text>
89
+ <Box height={1} flexShrink={0} />
90
+ <Text color={theme.inactive}>{centerLine(`mixdog coding agent · v${localPackageVersion()} · ${state.cwd}`, frameColumns, 4)}</Text>
91
+ </Box>
92
+ ) : null}
93
+
94
+ {/* Transcript viewport — a BOUNDED, fixed-height clipping box. The explicit
95
+ numeric height + overflow:hidden is what lets ink actually slice the
96
+ off-screen rows (output.clip in render-node-to-output.js), so older
97
+ rows can never overprint newer ones. justifyContent flex-end keeps the
98
+ newest content pinned to the bottom edge; older content overflows the
99
+ TOP and is clipped. flexShrink lets it yield rows to the live status /
100
+ a multi-line input rather than overflow the screen. */}
101
+ <Box
102
+ flexDirection="column"
103
+ width="100%"
104
+ height={viewportHeight}
105
+ flexGrow={0}
106
+ flexShrink={1}
107
+ overflow="hidden"
108
+ justifyContent="flex-end"
109
+ >
110
+ <Box
111
+ flexDirection="column"
112
+ width="100%"
113
+ height={transcriptContentHeight}
114
+ flexShrink={0}
115
+ overflow="hidden"
116
+ justifyContent="flex-end"
117
+ >
118
+ {/* Wheel scroll: with the viewport bottom-anchored (flex-end), a NEGATIVE
119
+ marginBottom pushes the transcript column DOWN past the bottom edge,
120
+ bringing older content above the window into view (overflow hidden
121
+ clips the newest rows that slide below). 0 = newest content pinned to
122
+ the bottom. (marginTop has no effect under flex-end — the bottom edge
123
+ stays fixed — so the scroll axis here is marginBottom, not marginTop.)
124
+ scrollOffset is clamped ≥ 0 by the wheel handler; a new turn snaps it
125
+ back to 0. */}
126
+ <Box flexDirection="column" width="100%" flexShrink={0} marginBottom={-transcriptWindow.effectiveScrollOffset}>
127
+ {/*
128
+ * Transcript windowing: render only the rows around the viewport rather
129
+ * than the full state.items list. A cheap bottom spacer preserves the
130
+ * same scroll coordinate when the visible window is in older history;
131
+ * items above the window are off-screen and omitted entirely.
132
+ * MAX cap: TRANSCRIPT_WINDOW_MAX_ITEMS items (env MIXDOG_TUI_TRANSCRIPT_WINDOW_ITEMS).
133
+ * OVERSCAN: TRANSCRIPT_WINDOW_OVERSCAN_ROWS extra rows above the viewport so
134
+ * fast wheel scrolls don't show a blank gap before re-render.
135
+ */}
136
+ {renderedTranscriptItems.map((item, i, arr) => {
137
+ const measureRef = transcriptMeasureRef(item);
138
+ const attachOverlayHint = overlayHintOnLastItem && i === overlayHintAttachItemIndex;
139
+ const itemNode = (
140
+ <Item
141
+ item={item}
142
+ prevKind={i > 0
143
+ ? arr[i - 1].kind
144
+ : (state.transcriptViewItems || state.items)[transcriptWindow.startIndex - 1]?.kind ?? null}
145
+ columns={frameColumns}
146
+ toolOutputExpanded={toolOutputExpanded}
147
+ rightMessage={attachOverlayHint ? inputHint : ''}
148
+ rightTone={attachOverlayHint ? inputHintTone : 'info'}
149
+ rightMessageWidth={attachOverlayHint ? (guardHintWidth || transientStatusWidth || 24) : 24}
150
+ themeEpoch={state.themeEpoch || 0}
151
+ streamingWindowRows={transcriptTailPinned && item.id === state.streamingTail?.id
152
+ ? transcriptContentHeight + TRANSCRIPT_WINDOW_TAIL_OVERSCAN_ROWS
153
+ : 0}
154
+ />
155
+ );
156
+ // When measured-rows is on, wrap each row in a zero-cost flex column
157
+ // whose ref exposes the row's REAL Yoga height to the harvest effect.
158
+ // The wrapper adds no rows of its own (it shrink-wraps the child) and
159
+ // is omitted entirely when the feature is disabled so the default
160
+ // render tree is byte-for-byte unchanged on the off path.
161
+ return measureRef ? (
162
+ <Box key={item.id} ref={measureRef} flexDirection="column" flexShrink={0}>
163
+ {itemNode}
164
+ </Box>
165
+ ) : (
166
+ <React.Fragment key={item.id}>{itemNode}</React.Fragment>
167
+ );
168
+ })}
169
+ {transcriptWindow.bottomSpacerRows > 0 ? (
170
+ <Box height={transcriptWindow.bottomSpacerRows} flexShrink={0} />
171
+ ) : null}
172
+ </Box>
173
+ </Box>
174
+ {welcomePromptHintRows > 0 ? (
175
+ <Box height={1} flexShrink={0} width="100%" overflow="hidden">
176
+ <Text color={theme.inactive} wrap="truncate">{centerLine(welcomePromptHintText, frameColumns, 2)}</Text>
177
+ </Box>
178
+ ) : null}
179
+ {panelCloseMaskRows > 0 ? (
180
+ <Box
181
+ height={panelCloseMaskRows}
182
+ flexShrink={0}
183
+ width="100%"
184
+ overflow="hidden"
185
+ backgroundColor={surfaceBackground()}
186
+ />
187
+ ) : null}
188
+ {overlayHintBandRows > 0 ? (
189
+ <Box height={1} flexShrink={0} backgroundColor={surfaceBackground()} flexDirection="row" width="100%" overflow="hidden">
190
+ <Box flexGrow={1} flexShrink={1} overflow="hidden" />
191
+ <Box flexShrink={0} width={guardHintWidth || 1} marginLeft={1} marginRight={1} justifyContent="flex-end" overflow="hidden">
192
+ <Text color={promptStatusColor(inputHintTone)} wrap="truncate">{inputHint}</Text>
193
+ </Box>
194
+ </Box>
195
+ ) : null}
196
+ {transcriptGuardRows > 0 ? (
197
+ <Box height={transcriptGuardRows} flexShrink={0} backgroundColor={surfaceBackground()} flexDirection="row" width="100%" overflow="hidden">
198
+ <Box flexGrow={1} flexShrink={1} overflow="hidden" />
199
+ {overlayHintFallbackRow && overlayHintBandRows === 0 ? (
200
+ <Box flexShrink={0} width={guardHintWidth || 1} marginLeft={1} marginRight={1} justifyContent="flex-end" overflow="hidden">
201
+ <Text color={promptStatusColor(inputHintTone)} wrap="truncate">{inputHint}</Text>
202
+ </Box>
203
+ ) : null}
204
+ </Box>
205
+ ) : null}
206
+ </Box>
207
+
208
+ {/* Live reasoning and transient status live just above the prompt: reasoning
209
+ on the left, short-lived copy/error/info messages on the right. */}
210
+
211
+ {/* Bottom bar — pinned to the physical bottom, never moves. Floating
212
+ panels use their actual rendered height and shrink before the prompt
213
+ can move; overflow is clipped from the top while the panel remains
214
+ bottom-aligned against the prompt. */}
215
+ <Box flexDirection="column" flexShrink={0} width="100%" backgroundColor={surfaceBackground()}>
216
+ {panelTransitionClearRows > 0 ? (
217
+ <Box height={panelTransitionClearRows} flexShrink={0} width="100%" overflow="hidden" backgroundColor={surfaceBackground()} />
218
+ ) : null}
219
+ {floatingPanelRows > 0 ? (
220
+ <Box flexDirection="column" flexShrink={0} height={floatingPanelRows} overflow="hidden" justifyContent="flex-end" backgroundColor={surfaceBackground()}>
221
+ {toolApproval ? (
222
+ <Picker
223
+ items={[
224
+ { value: 'deny', label: 'Deny', marker: '×', markerColor: theme.error, description: 'block this tool call' },
225
+ { value: 'approve', label: 'Approve once', marker: '✓', markerColor: theme.success, description: 'run this tool call' },
226
+ ]}
227
+ onSelect={(value) => {
228
+ store.resolveToolApproval?.(toolApproval.id, {
229
+ approved: value === 'approve',
230
+ reason: value === 'approve' ? 'approved by user' : 'denied by user',
231
+ });
232
+ }}
233
+ onCancel={() => {
234
+ store.resolveToolApproval?.(toolApproval.id, { approved: false, reason: 'denied by user' });
235
+ }}
236
+ onKey={(input) => {
237
+ const value = String(input || '').trim().toLowerCase();
238
+ if (value === 'a' || value === 'y') {
239
+ store.resolveToolApproval?.(toolApproval.id, { approved: true, reason: 'approved by user' });
240
+ } else if (value === 'd' || value === 'n') {
241
+ store.resolveToolApproval?.(toolApproval.id, { approved: false, reason: 'denied by user' });
242
+ }
243
+ }}
244
+ title="Tool approval"
245
+ description={toolApprovalDescription(toolApproval)}
246
+ help="↑/↓ Select · Enter Choose · a/y Approve · d/n/Esc Deny"
247
+ columns={frameColumns}
248
+ labelWidth={18}
249
+ initialIndex={0}
250
+ indexMode="never"
251
+ visibleCount={2}
252
+ fillHeight={expandedOptionPanel}
253
+ />
254
+ ) : picker ? (
255
+ <Picker
256
+ key={picker.pickerKey}
257
+ items={picker.items}
258
+ onSelect={(value, item) => {
259
+ pickerOpenedFromEnterRef.current = true;
260
+ if (pickerOpenedFromEnterTimerRef.current) {
261
+ clearTimeout(pickerOpenedFromEnterTimerRef.current);
262
+ pickerOpenedFromEnterTimerRef.current = null;
263
+ }
264
+ try {
265
+ if (picker.onSelect) picker.onSelect(value, item);
266
+ } finally {
267
+ pickerOpenedFromEnterTimerRef.current = setTimeout(() => {
268
+ pickerOpenedFromEnterRef.current = false;
269
+ pickerOpenedFromEnterTimerRef.current = null;
270
+ }, 3000);
271
+ }
272
+ }}
273
+ onCancel={() => {
274
+ if (picker.onCancel) picker.onCancel();
275
+ else {
276
+ setPicker(null);
277
+ clearPromptHint();
278
+ }
279
+ }}
280
+ onLeft={picker.onLeft}
281
+ onRight={picker.onRight}
282
+ onTab={picker.onTab}
283
+ onKey={picker.onKey}
284
+ onHighlight={picker.onHighlight}
285
+ title={picker.title}
286
+ description={picker.description}
287
+ footer={picker.footer}
288
+ footerGapRows={picker.footerGapRows}
289
+ help={picker.help}
290
+ columns={frameColumns}
291
+ labelWidth={picker.labelWidth}
292
+ metaWidth={picker.metaWidth}
293
+ initialIndex={picker.initialIndex}
294
+ indexMode={picker.indexMode}
295
+ visibleCount={pickerVisibleRows}
296
+ fillHeight={expandedOptionPanel}
297
+ themeEpoch={state.themeEpoch || 0}
298
+ confirmBar={picker.confirmBar}
299
+ />
300
+ ) : contextPanel ? (
301
+ <ContextPanel
302
+ rows={contextPanel.rows}
303
+ title={contextPanel.title}
304
+ detail={contextPanel.detail}
305
+ columns={frameColumns}
306
+ fillHeight={expandedOptionPanel}
307
+ />
308
+ ) : usagePanel ? (
309
+ <UsagePanel
310
+ dashboard={usagePanel}
311
+ columns={frameColumns}
312
+ fillHeight={expandedOptionPanel}
313
+ panelRows={floatingPanelRows}
314
+ />
315
+ ) : slashPaletteOpen ? (
316
+ <SlashCommandPalette
317
+ commands={slashCommands}
318
+ selectedIndex={slashIndex}
319
+ title="Commands"
320
+ columns={frameColumns}
321
+ query={activeSlashQuery}
322
+ />
323
+ ) : providerPrompt ? (
324
+ <TextEntryPanel
325
+ title={providerPrompt.kind === 'api-key'
326
+ ? `${providerPrompt.mode === 'replace' ? 'Replace' : 'Set'} API key · ${providerPrompt.label}`
327
+ : providerPrompt.kind === 'oauth-code'
328
+ ? providerPrompt.label
329
+ : providerPrompt.kind === 'openai-usage-session'
330
+ ? 'OpenAI Usage · Session Key'
331
+ : `Base URL · ${providerPrompt.label}`}
332
+ hint={providerPrompt.kind === 'api-key'
333
+ ? [
334
+ providerPrompt.envName ? `Env: ${providerPrompt.envName}` : '',
335
+ providerPrompt.source ? `Current: ${providerPrompt.source}` : '',
336
+ 'Stored in the OS keychain.',
337
+ ].filter(Boolean).join(' · ')
338
+ : providerPrompt.kind === 'oauth-code'
339
+ ? (providerPrompt.hint || 'Paste the browser code.')
340
+ : providerPrompt.kind === 'openai-usage-session'
341
+ ? 'Paste an OpenAI dashboard/session key for the undocumented credit lookup. It is stored in the OS keychain.'
342
+ : `Default: ${providerPrompt.defaultURL}`}
343
+ detail={providerPrompt.detail || ''}
344
+ mask={providerPrompt.kind === 'api-key' || providerPrompt.kind === 'openai-usage-session'}
345
+ columns={frameColumns}
346
+ actionLabel={providerPrompt.kind === 'oauth-code' ? 'continue' : 'save'}
347
+ promptLabel={providerPrompt.kind === 'api-key'
348
+ ? 'API key > '
349
+ : providerPrompt.kind === 'oauth-code'
350
+ ? 'Paste code here if prompted > '
351
+ : providerPrompt.kind === 'openai-usage-session'
352
+ ? 'Session key > '
353
+ : 'Base URL > '}
354
+ onSubmit={onSubmit}
355
+ onCancel={cancelProviderPrompt}
356
+ />
357
+ ) : channelPrompt ? (
358
+ <TextEntryPanel
359
+ title={channelPrompt.label}
360
+ hint={channelPrompt.hint || 'Save channel setting.'}
361
+ mask={channelPrompt.kind === 'discord-token' || channelPrompt.kind === 'telegram-token' || channelPrompt.kind === 'webhook-token'}
362
+ columns={frameColumns}
363
+ promptLabel="Value > "
364
+ onSubmit={onSubmit}
365
+ onCancel={cancelChannelPrompt}
366
+ />
367
+ ) : hookPrompt ? (
368
+ <TextEntryPanel
369
+ title={hookPrompt.label}
370
+ hint={hookPrompt.hint || 'Save hook setting.'}
371
+ columns={frameColumns}
372
+ promptLabel="Value > "
373
+ onSubmit={onSubmit}
374
+ onCancel={cancelHookPrompt}
375
+ />
376
+ ) : settingsPrompt ? (
377
+ <TextEntryPanel
378
+ title={settingsPrompt.label}
379
+ hint={settingsPrompt.hint || 'Save setting.'}
380
+ columns={frameColumns}
381
+ initialValue={settingsPrompt.initialValue || ''}
382
+ multiline={settingsPrompt.kind === 'core-add' || settingsPrompt.kind === 'core-edit'}
383
+ maxContentRows={PANEL_MAX_VISIBLE}
384
+ onContentRowsChange={setTextEntryLayoutRows}
385
+ actionLabel={settingsPrompt.kind === 'skill-use'
386
+ ? 'run'
387
+ : settingsPrompt.kind === 'autoclear-provider'
388
+ ? 'save'
389
+ : settingsPrompt.kind === 'project-new'
390
+ ? 'open'
391
+ : settingsPrompt.kind === 'project-create-confirm'
392
+ ? 'confirm'
393
+ : settingsPrompt.kind === 'project-rename'
394
+ ? 'rename'
395
+ : settingsPrompt.kind === 'core-add'
396
+ ? 'add'
397
+ : settingsPrompt.kind === 'core-edit'
398
+ ? 'save'
399
+ : settingsPrompt.kind === 'core-delete-confirm'
400
+ ? 'confirm'
401
+ : 'save'}
402
+ promptLabel={settingsPrompt.kind === 'skill-use'
403
+ ? 'Command > '
404
+ : settingsPrompt.kind === 'autoclear-provider'
405
+ ? 'Duration > '
406
+ : settingsPrompt.kind === 'project-new'
407
+ ? 'Path > '
408
+ : settingsPrompt.kind === 'project-create-confirm'
409
+ ? 'Create? (y/n) > '
410
+ : settingsPrompt.kind === 'project-rename'
411
+ ? 'Name > '
412
+ : settingsPrompt.kind === 'core-add'
413
+ ? 'Sentence > '
414
+ : settingsPrompt.kind === 'core-edit'
415
+ ? 'Sentence > '
416
+ : settingsPrompt.kind === 'core-delete-confirm'
417
+ ? 'Delete? (y/n) > '
418
+ : 'Value > '}
419
+ onSubmit={onSubmit}
420
+ onCancel={cancelSettingsPrompt}
421
+ />
422
+ ) : null}
423
+ </Box>
424
+ ) : null}
425
+ {!inputBoxHidden ? (
426
+ <>
427
+ {promptMetaVisible ? (
428
+ <>
429
+ <Box
430
+ marginTop={0}
431
+ marginBottom={0}
432
+ height={1}
433
+ width="100%"
434
+ flexDirection="row"
435
+ backgroundColor={surfaceBackground()}
436
+ >
437
+ <Box flexGrow={1} flexShrink={1} overflow="hidden">
438
+ {liveSpinner ? (
439
+ <Spinner
440
+ verb={liveSpinner.verb}
441
+ startedAt={liveSpinner.startedAt}
442
+ outputTokens={liveSpinner?.outputTokens ?? liveSpinner?.tokens ?? 0}
443
+ thinking={!!(state.thinking || liveSpinner?.thinking)}
444
+ thinkingActiveSince={liveSpinner?.thinkingSegmentStartedAt ?? 0}
445
+ mode={liveSpinner?.mode || 'responding'}
446
+ columns={promptSpinnerColumns}
447
+ marginTop={0}
448
+ />
449
+ ) : null}
450
+ </Box>
451
+ {inputHint ? (
452
+ <Box flexShrink={0} width={transientStatusWidth || 1} marginLeft={1} marginRight={1} justifyContent="flex-end" overflow="hidden">
453
+ <Text color={promptStatusColor(inputHintTone)} wrap="truncate">{inputHint}</Text>
454
+ </Box>
455
+ ) : null}
456
+ </Box>
457
+ <Box height={1} width="100%" backgroundColor={surfaceBackground()} />
458
+ </>
459
+ ) : null}
460
+ {queuedVisible ? (
461
+ <QueuedCommands queued={state.queued} columns={frameColumns} compact={queuedCompact} />
462
+ ) : null}
463
+ <Box
464
+ marginTop={0}
465
+ width="100%"
466
+ height={promptBoxRows}
467
+ flexShrink={0}
468
+ borderStyle="round"
469
+ borderColor={theme.promptBorder}
470
+ backgroundColor={surfaceBackground()}
471
+ paddingX={1}
472
+ >
473
+ {promptInputControl}
474
+ </Box>
475
+ </>
476
+ ) : null}
477
+ <StatusLine
478
+ sessionId={state.sessionId}
479
+ clientHostPid={state.ownerClientHostPid || state.clientHostPid}
480
+ provider={state.provider}
481
+ model={state.model}
482
+ effort={state.effort}
483
+ fast={state.fast}
484
+ cwd={state.cwd}
485
+ stats={statuslineStats}
486
+ contextWindow={state.contextWindow}
487
+ displayContextWindow={state.displayContextWindow}
488
+ compactBoundaryTokens={state.compactBoundaryTokens}
489
+ autoCompactTokenLimit={state.autoCompactTokenLimit}
490
+ rawContextWindow={state.rawContextWindow}
491
+ resizeEpoch={resizeEpoch}
492
+ agentRevision={agentRevision}
493
+ agentWorkers={state.agentWorkers}
494
+ agentJobs={state.agentJobs}
495
+ activeTools={activeTools}
496
+ initialLine={initialStatusLine}
497
+ workflow={state.workflow}
498
+ remoteEnabled={state.remoteEnabled === true}
499
+ themeEpoch={state.themeEpoch || 0}
500
+ />
501
+ </Box>
502
+ </Box>
503
+ );
504
+ }
@@ -309,12 +309,13 @@ export function createChannelPickers({
309
309
  value: 'webhook-endpoint',
310
310
  label: 'Webhook endpoint',
311
311
  // Relay tunnel: public exposure is automatic, so the endpoint is
312
- // "On" whenever the webhook server itself is enabled.
312
+ // "On" whenever the webhook server itself is enabled. Info-only row:
313
+ // endpoint management lives in the desktop Webhooks page.
313
314
  meta: setup.webhook?.enabled === false ? 'Off' : 'On',
314
315
  description: setup.webhook?.publicUrl
315
316
  ? setup.webhook.publicUrl
316
317
  : 'Mixdog relay tunnel — URL assigned on first run',
317
- _action: 'webhook-endpoint',
318
+ _action: null,
318
319
  },
319
320
  ];
320
321
 
@@ -361,12 +362,6 @@ export function createChannelPickers({
361
362
  });
362
363
  return;
363
364
  }
364
- if (item._action === 'webhook-endpoint') {
365
- void openChannelSetupPicker('webhook-endpoint', {
366
- ...(options.onboarding ? { onboarding: true, confirmBar: options.confirmBar || null } : {}),
367
- returnTo: () => reopenRoot({ highlightValue: 'webhook-endpoint' }),
368
- });
369
- }
370
365
  } catch (e) {
371
366
  store.pushNotice(`channels update failed: ${e?.message || e}`, 'error');
372
367
  }