@aragon-agent/cli 0.5.0 → 0.5.2
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 +158 -0
- package/README.md +254 -7
- package/dist/agent/agent-mode.d.ts +53 -0
- package/dist/agent/agent-mode.js +69 -0
- package/dist/agent/controller.d.ts +69 -0
- package/dist/agent/controller.js +154 -1
- package/dist/agent/plan-prompt.d.ts +18 -0
- package/dist/agent/plan-prompt.js +93 -0
- package/dist/agent/reducer.d.ts +14 -1
- package/dist/agent/reducer.js +9 -0
- package/dist/agent/system-prompt.d.ts +15 -0
- package/dist/agent/system-prompt.js +8 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +247 -23
- package/dist/commands/builtins.js +60 -0
- package/dist/commands/registry.d.ts +17 -0
- package/dist/config/app-paths.d.ts +71 -4
- package/dist/config/app-paths.js +139 -8
- package/dist/config/cli-commands.d.ts +43 -0
- package/dist/config/cli-commands.js +196 -0
- package/dist/config/env.d.ts +7 -1
- package/dist/config/env.js +53 -1
- package/dist/config/load.d.ts +10 -0
- package/dist/config/load.js +82 -2
- package/dist/config/migrate-home.d.ts +63 -0
- package/dist/config/migrate-home.js +165 -0
- package/dist/config/migrate-legacy-state.d.ts +10 -0
- package/dist/config/migrate-legacy-state.js +15 -6
- package/dist/config/schema.d.ts +81 -0
- package/dist/config/schema.js +85 -0
- package/dist/config/store.d.ts +30 -12
- package/dist/config/store.js +47 -35
- package/dist/input/mouse-events.d.ts +61 -0
- package/dist/input/mouse-events.js +153 -0
- package/dist/input/stdin-mouse-filter.d.ts +42 -0
- package/dist/input/stdin-mouse-filter.js +153 -0
- package/dist/logging/cli-commands.d.ts +25 -0
- package/dist/logging/cli-commands.js +221 -0
- package/dist/logging/file-sink.d.ts +114 -0
- package/dist/logging/file-sink.js +326 -0
- package/dist/logging/install.d.ts +163 -0
- package/dist/logging/install.js +302 -0
- package/dist/logging/levels.d.ts +40 -0
- package/dist/logging/levels.js +59 -0
- package/dist/logging/logger.d.ts +110 -0
- package/dist/logging/logger.js +237 -0
- package/dist/logging/redact.d.ts +28 -0
- package/dist/logging/redact.js +90 -0
- package/dist/logging/secret-registry.d.ts +30 -0
- package/dist/logging/secret-registry.js +55 -0
- package/dist/skills/paths.d.ts +2 -4
- package/dist/skills/paths.js +7 -9
- package/dist/tools/human-input.d.ts +179 -0
- package/dist/tools/human-input.js +227 -0
- package/dist/tools/index.d.ts +35 -3
- package/dist/tools/index.js +96 -9
- package/dist/tools/plan-tools.d.ts +64 -0
- package/dist/tools/plan-tools.js +198 -0
- package/dist/ui/App.d.ts +12 -1
- package/dist/ui/App.js +223 -17
- package/dist/ui/Composer.d.ts +9 -1
- package/dist/ui/Composer.js +26 -7
- package/dist/ui/ModeChip.d.ts +29 -0
- package/dist/ui/ModeChip.js +15 -0
- package/dist/ui/PromptInput.d.ts +22 -1
- package/dist/ui/PromptInput.js +49 -2
- package/dist/ui/StatusBar.d.ts +15 -0
- package/dist/ui/StatusBar.js +10 -2
- package/dist/ui/entries/ToolPreview.d.ts +2 -0
- package/dist/ui/entries/ToolPreview.js +64 -0
- package/dist/ui/glyphs.d.ts +21 -0
- package/dist/ui/glyphs.js +12 -0
- package/dist/ui/layout/AppShell.d.ts +10 -1
- package/dist/ui/layout/AppShell.js +38 -3
- package/dist/ui/layout/ScrollIndicator.d.ts +31 -0
- package/dist/ui/layout/ScrollIndicator.js +15 -0
- package/dist/ui/layout/ScrollViewport.d.ts +8 -2
- package/dist/ui/layout/ScrollViewport.js +24 -4
- package/dist/ui/layout/regions.d.ts +55 -0
- package/dist/ui/layout/regions.js +52 -0
- package/dist/ui/layout/scroll-indicator.d.ts +27 -0
- package/dist/ui/layout/scroll-indicator.js +35 -0
- package/dist/ui/layout/scroll.d.ts +9 -0
- package/dist/ui/layout/scroll.js +23 -0
- package/dist/ui/layout/wrap-rows.d.ts +24 -0
- package/dist/ui/layout/wrap-rows.js +74 -0
- package/dist/ui/overlays/HelpOverlay.js +15 -0
- package/dist/ui/overlays/PlanReviewOverlay.d.ts +50 -0
- package/dist/ui/overlays/PlanReviewOverlay.js +117 -0
- package/dist/ui/overlays/QuestionOverlay.d.ts +30 -0
- package/dist/ui/overlays/QuestionOverlay.js +193 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +7 -0
- package/dist/ui/overlays/SettingsScreen.js +8 -1
- package/dist/ui/screen.d.ts +14 -1
- package/dist/ui/screen.js +37 -3
- package/dist/ui/use-startup-notices.d.ts +25 -0
- package/dist/ui/use-startup-notices.js +64 -0
- package/dist/ui/use-wheel-routing.d.ts +68 -0
- package/dist/ui/use-wheel-routing.js +139 -0
- package/package.json +3 -2
- package/runtime/insecure-tls-warning.cjs +62 -0
- package/runtime/insecure-tls-warning.d.cts +5 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ask_user` and `submit_plan` — the two tools that only exist because a human
|
|
3
|
+
* is attached (plan-mode §4.1 / §4.2 / §5.3).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `tools/` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* THE SCHEMAS DECLARE SHAPE, NOT POLICY (P0-4). `type`, `required` and `default`
|
|
8
|
+
* stay, because a `questions` value that is not an array has nothing for a
|
|
9
|
+
* normalizer to repair and both validator strategies agree on that check.
|
|
10
|
+
* Everything else — how many questions, how many options, how long a header may
|
|
11
|
+
* be — lives in the `description`, which is what the model actually reads, and
|
|
12
|
+
* is enforced by `normalizeQuestions` / `normalizePlan`. Putting `maxItems: 5`
|
|
13
|
+
* in the schema instead had two failure modes at once: with `ajv` present
|
|
14
|
+
* `ToolExecutor` rejected a six-question call outright and the repair rules were
|
|
15
|
+
* dead code; without it (ajv is an OPTIONAL dependency of core) the same call
|
|
16
|
+
* sailed straight through to the repair path. One design, two behaviours,
|
|
17
|
+
* selected by whether an optional install step succeeded.
|
|
18
|
+
*/
|
|
19
|
+
import { errorResult, textResult } from '@aragon-agent/core';
|
|
20
|
+
import { formatAnswersResult, normalizePlan, normalizeQuestions, } from './human-input.js';
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Shared refusal text
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
const NO_INTERACTIVE_USER = 'No interactive user is attached (-p/--print mode). Do not ask questions: ' +
|
|
25
|
+
'state your assumptions explicitly and continue.';
|
|
26
|
+
const NO_INTERACTIVE_REVIEWER = 'No interactive reviewer is attached. Emit the full plan as your final answer ' +
|
|
27
|
+
'in markdown instead.';
|
|
28
|
+
function roundCapRefusal(used) {
|
|
29
|
+
return (`You have already asked ${used} rounds of questions (the configured maximum). ` +
|
|
30
|
+
'Proceed with your best assumptions and state them in the plan.');
|
|
31
|
+
}
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// ask_user
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
const ASK_USER_DESCRIPTION = 'Ask the user 1-5 multiple-choice questions when a decision would change the work. ' +
|
|
36
|
+
'Give each question a stable id, a header of at most 12 characters, and 2-4 options; ' +
|
|
37
|
+
'exactly one option must set recommended:true, with the reason in its description. ' +
|
|
38
|
+
'Extra questions or options beyond these limits are dropped, so keep within them. ' +
|
|
39
|
+
'Do not ask anything you could answer by reading the workspace.';
|
|
40
|
+
function makeAskUser(opts) {
|
|
41
|
+
return {
|
|
42
|
+
name: 'ask_user',
|
|
43
|
+
label: 'Ask the user',
|
|
44
|
+
description: ASK_USER_DESCRIPTION,
|
|
45
|
+
parameters: {
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
questions: {
|
|
49
|
+
type: 'array',
|
|
50
|
+
description: '1-5 questions, asked in one round.',
|
|
51
|
+
items: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {
|
|
54
|
+
id: { type: 'string', description: 'Stable key echoed back in the answer.' },
|
|
55
|
+
header: {
|
|
56
|
+
type: 'string',
|
|
57
|
+
description: 'Very short chip label (<= 12 chars), e.g. "Datastore".',
|
|
58
|
+
},
|
|
59
|
+
question: { type: 'string', description: '<= 300 chars.' },
|
|
60
|
+
allowMultiple: { type: 'boolean', default: false },
|
|
61
|
+
options: {
|
|
62
|
+
type: 'array',
|
|
63
|
+
description: '2-4 options. Exactly one must be recommended.',
|
|
64
|
+
items: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: {
|
|
67
|
+
label: { type: 'string', description: '<= 60 chars.' },
|
|
68
|
+
description: {
|
|
69
|
+
type: 'string',
|
|
70
|
+
description: '<= 120 chars. Why this option.',
|
|
71
|
+
},
|
|
72
|
+
recommended: { type: 'boolean', default: false },
|
|
73
|
+
},
|
|
74
|
+
required: ['label'],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
required: ['id', 'header', 'question', 'options'],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
required: ['questions'],
|
|
83
|
+
},
|
|
84
|
+
async execute(_id, params, ctx) {
|
|
85
|
+
// Probed BEFORE the work, never discovered afterwards: a missing human
|
|
86
|
+
// channel must mean "refuse with an actionable message", not "wait
|
|
87
|
+
// forever" and not "assume yes".
|
|
88
|
+
if (!opts.gate.canPrompt())
|
|
89
|
+
return errorResult(NO_INTERACTIVE_USER);
|
|
90
|
+
const questions = normalizeQuestions(params.questions);
|
|
91
|
+
if (questions.length === 0) {
|
|
92
|
+
return errorResult('No usable questions: each needs 2-4 distinct options.');
|
|
93
|
+
}
|
|
94
|
+
const ticket = opts.takeAskRound();
|
|
95
|
+
if (!ticket.ok)
|
|
96
|
+
return errorResult(roundCapRefusal(ticket.used));
|
|
97
|
+
const response = await opts.withHumanWait(() => opts.gate.request({ kind: 'questions', questions }, ctx.signal));
|
|
98
|
+
// `null` covers cancel, unmount, abort AND timeout. From the model's point
|
|
99
|
+
// of view an unanswered question and an abandoned one are the same
|
|
100
|
+
// situation and want the same guidance, so they share a shape — and it is
|
|
101
|
+
// a NON-error result on purpose: a cancellation rendered as a tool failure
|
|
102
|
+
// invites a retry loop.
|
|
103
|
+
const cancelled = response === null || response.kind !== 'answers' || response.cancelled;
|
|
104
|
+
const answers = response !== null && response.kind === 'answers' ? response.answers : [];
|
|
105
|
+
return textResult(formatAnswersResult({
|
|
106
|
+
answers,
|
|
107
|
+
cancelled,
|
|
108
|
+
roundsUsed: ticket.used,
|
|
109
|
+
roundsRemaining: ticket.remaining,
|
|
110
|
+
}, questions.length));
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// submit_plan
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
const SUBMIT_PLAN_DESCRIPTION = 'Present the implementation plan for approval. Call this exactly once, after you ' +
|
|
118
|
+
'have read enough of the workspace to be specific. Steps must be ordered and ' +
|
|
119
|
+
'independently verifiable. Limits, enforced by truncation rather than rejection: ' +
|
|
120
|
+
'title <= 80 chars, summary <= 600, at most 20 steps (title <= 100, detail <= 400), ' +
|
|
121
|
+
'at most 30 filesTouched, 10 risks, 10 openQuestions.';
|
|
122
|
+
const APPROVED_RESULT = 'Plan approved. You are now in Build mode - implement the plan step by step, in order.';
|
|
123
|
+
const DISMISSED_RESULT = 'The user dismissed the plan review without a verdict. Ask a clarifying question ' +
|
|
124
|
+
'or refine the plan, then call submit_plan again.';
|
|
125
|
+
function makeSubmitPlan(opts) {
|
|
126
|
+
return {
|
|
127
|
+
name: 'submit_plan',
|
|
128
|
+
label: 'Submit plan',
|
|
129
|
+
description: SUBMIT_PLAN_DESCRIPTION,
|
|
130
|
+
parameters: {
|
|
131
|
+
type: 'object',
|
|
132
|
+
properties: {
|
|
133
|
+
title: { type: 'string' },
|
|
134
|
+
summary: { type: 'string' },
|
|
135
|
+
steps: {
|
|
136
|
+
type: 'array',
|
|
137
|
+
items: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
properties: {
|
|
140
|
+
title: { type: 'string' },
|
|
141
|
+
detail: { type: 'string' },
|
|
142
|
+
},
|
|
143
|
+
required: ['title'],
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
filesTouched: { type: 'array', items: { type: 'string' } },
|
|
147
|
+
risks: { type: 'array', items: { type: 'string' } },
|
|
148
|
+
openQuestions: { type: 'array', items: { type: 'string' } },
|
|
149
|
+
},
|
|
150
|
+
required: ['title', 'summary', 'steps'],
|
|
151
|
+
},
|
|
152
|
+
async execute(_id, params, ctx) {
|
|
153
|
+
// Non-error: under `-p` the read-only gate still applies, so "plan this
|
|
154
|
+
// and do not touch my repository" is a legitimate combination and must not
|
|
155
|
+
// read as a malfunction.
|
|
156
|
+
if (!opts.gate.canPrompt())
|
|
157
|
+
return textResult(NO_INTERACTIVE_REVIEWER);
|
|
158
|
+
const plan = normalizePlan(params);
|
|
159
|
+
if (!plan)
|
|
160
|
+
return errorResult('A plan needs at least one step with a title.');
|
|
161
|
+
const response = await opts.withHumanWait(() => opts.gate.request({ kind: 'plan', plan }, ctx.signal));
|
|
162
|
+
if (response === null || response.kind !== 'planDecision') {
|
|
163
|
+
return textResult(DISMISSED_RESULT);
|
|
164
|
+
}
|
|
165
|
+
if (response.decision === 'approved') {
|
|
166
|
+
opts.onPlanApproved();
|
|
167
|
+
return textResult(APPROVED_RESULT);
|
|
168
|
+
}
|
|
169
|
+
if (response.decision === 'revise') {
|
|
170
|
+
return textResult(`Plan rejected. The user's feedback: "${response.feedback}". ` +
|
|
171
|
+
'Revise and call submit_plan again.');
|
|
172
|
+
}
|
|
173
|
+
return textResult(DISMISSED_RESULT);
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Registration
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
/**
|
|
181
|
+
* Build the plan tools, or `[]` for a channel that can never reach a human.
|
|
182
|
+
*
|
|
183
|
+
* KEYED ON `neverPrompts`, NOT `canPrompt()` (P0-3). This function runs inside
|
|
184
|
+
* `makeController()`, and in the interactive path the bridge handler is still
|
|
185
|
+
* null there — the App attaches it later, in an effect. So `canPrompt()` is
|
|
186
|
+
* false for EVERY session at this moment, and gating registration on it would
|
|
187
|
+
* ship plan mode with no `ask_user` and no `submit_plan`, in every interactive
|
|
188
|
+
* session, with nothing anywhere reporting it.
|
|
189
|
+
*
|
|
190
|
+
* Registration is a one-time structural question ("can this channel ever reach a
|
|
191
|
+
* human?"). `canPrompt()` answers a live one ("is one attached right now?") and
|
|
192
|
+
* is checked again inside each tool, where it belongs.
|
|
193
|
+
*/
|
|
194
|
+
export function createPlanTools(opts) {
|
|
195
|
+
if (opts.gate.neverPrompts)
|
|
196
|
+
return [];
|
|
197
|
+
return [makeAskUser(opts), makeSubmitPlan(opts)];
|
|
198
|
+
}
|
package/dist/ui/App.d.ts
CHANGED
|
@@ -9,6 +9,9 @@ import type { AgentController } from '../agent/controller.js';
|
|
|
9
9
|
import type { ConfirmRequest } from '../tools/index.js';
|
|
10
10
|
import { type Overlay } from '../agent/reducer.js';
|
|
11
11
|
import { type RenderMode } from './layout/frame.js';
|
|
12
|
+
import { type FrameGeometry } from './layout/regions.js';
|
|
13
|
+
import type { MouseSource } from '../input/stdin-mouse-filter.js';
|
|
14
|
+
import type { HumanInputBridge } from '../tools/human-input.js';
|
|
12
15
|
export interface ConfirmBridge {
|
|
13
16
|
handler: ((req: ConfirmRequest) => Promise<boolean>) | null;
|
|
14
17
|
}
|
|
@@ -20,5 +23,13 @@ export interface AppProps {
|
|
|
20
23
|
initialOverlay?: Overlay;
|
|
21
24
|
initialPrompt?: string;
|
|
22
25
|
confirmBridge?: ConfirmBridge;
|
|
26
|
+
/** The plan-mode human channel; absent in tests that do not exercise it. */
|
|
27
|
+
humanInputBridge?: HumanInputBridge;
|
|
28
|
+
/** Wheel events, already stripped out of stdin. Absent whenever mouse support
|
|
29
|
+
* is off: inline mode, `--no-mouse`, a non-TTY, or a filter that failed to
|
|
30
|
+
* build (mouse-wheel-region-routing §4.2). */
|
|
31
|
+
mouseSource?: MouseSource;
|
|
32
|
+
/** Test-only seam (§8.2): frame geometry, bypassing `measureElement`. */
|
|
33
|
+
geometryOverride?: FrameGeometry;
|
|
23
34
|
}
|
|
24
|
-
export declare function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, }: AppProps): React.ReactElement;
|
|
35
|
+
export declare function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, geometryOverride, }: AppProps): React.ReactElement;
|
package/dist/ui/App.js
CHANGED
|
@@ -12,7 +12,10 @@ import { initialViewState, reduceEvent, viewReducer, } from '../agent/reducer.js
|
|
|
12
12
|
import { mergeDeltas } from '../agent/coalesce.js';
|
|
13
13
|
import { coerceMaxTokens, DEFAULT_TRANSCRIPT_WINDOW, PROMPT_HISTORY_CAP, } from '../config/schema.js';
|
|
14
14
|
import { updatePersistedConfig } from '../config/store.js';
|
|
15
|
+
import { getLogger } from '../logging/logger.js';
|
|
16
|
+
import { registerSecret } from '../logging/secret-registry.js';
|
|
15
17
|
import { detectCapabilities } from './capabilities.js';
|
|
18
|
+
import { useStartupNotices } from './use-startup-notices.js';
|
|
16
19
|
import { getTheme } from './theme.js';
|
|
17
20
|
import { pickGlyphs } from './glyphs.js';
|
|
18
21
|
import { Header } from './Header.js';
|
|
@@ -28,6 +31,8 @@ import { ScrollViewport } from './layout/ScrollViewport.js';
|
|
|
28
31
|
import { useTerminalSize } from './layout/useTerminalSize.js';
|
|
29
32
|
import { frameHeight, MIN_FULLSCREEN_ROWS } from './layout/frame.js';
|
|
30
33
|
import { HINT_MIN_ROWS, viewportRows as computeViewportRows } from './layout/budget.js';
|
|
34
|
+
import { UNMEASURED_GEOMETRY } from './layout/regions.js';
|
|
35
|
+
import { OVERLAY_PAGE, useWheelRouting } from './use-wheel-routing.js';
|
|
31
36
|
import { installConsoleBridge } from './console-bridge.js';
|
|
32
37
|
import { publishExitSnapshot } from './exit-snapshot.js';
|
|
33
38
|
import { OverlayFrame } from './layout/OverlayFrame.js';
|
|
@@ -35,14 +40,15 @@ import { helpRows } from './overlays/HelpOverlay.js';
|
|
|
35
40
|
import { ModelPicker } from './overlays/ModelPicker.js';
|
|
36
41
|
import { SettingsScreen } from './overlays/SettingsScreen.js';
|
|
37
42
|
import { ConfirmDialog } from './overlays/ConfirmDialog.js';
|
|
43
|
+
import { QuestionOverlay } from './overlays/QuestionOverlay.js';
|
|
44
|
+
import { PlanReviewOverlay } from './overlays/PlanReviewOverlay.js';
|
|
38
45
|
import { CommandRegistry, runSlashInput } from '../commands/registry.js';
|
|
39
46
|
import { registerBuiltinCommands } from '../commands/builtins.js';
|
|
40
47
|
import { makeSkillsCommand, registerSkillCommands } from '../commands/skills.js';
|
|
48
|
+
import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
|
|
41
49
|
/** Merge pending delta actions ~33 ms and flush the coalescer buffer. */
|
|
42
50
|
const COALESCE_MS = 33;
|
|
43
|
-
|
|
44
|
-
const OVERLAY_PAGE = 8;
|
|
45
|
-
export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, }) {
|
|
51
|
+
export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, geometryOverride, }) {
|
|
46
52
|
const { exit } = useApp();
|
|
47
53
|
const { stdout } = useStdout();
|
|
48
54
|
const fullscreen = mode === 'fullscreen';
|
|
@@ -50,9 +56,13 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
50
56
|
const [state, dispatch] = useReducer(viewReducer, undefined, initialViewState);
|
|
51
57
|
const [elapsedMs, setElapsedMs] = useState(0);
|
|
52
58
|
const [confirmState, setConfirmState] = useState(null);
|
|
59
|
+
/** The request the question / plan overlay is currently rendering. */
|
|
60
|
+
const [humanRequest, setHumanRequest] = useState(null);
|
|
53
61
|
const [promptHistory, setPromptHistory] = useState(() => controller.getConfig().promptHistory ?? []);
|
|
54
62
|
// Scroll plumbing: intent goes down, one derived display number comes back.
|
|
55
63
|
const [scrollIntent, setScrollIntent] = useState();
|
|
64
|
+
/** One prompt-history step per wheel notch over the composer band (§4.7). */
|
|
65
|
+
const [historyIntent, setHistoryIntent] = useState();
|
|
56
66
|
const [pinToBottomNonce, setPinToBottomNonce] = useState(0);
|
|
57
67
|
const [scrolledLines, setScrolledLines] = useState(0);
|
|
58
68
|
// I-5 (§4.13): Ctrl+L cannot repaint by writing escapes — Ink dedupes
|
|
@@ -87,6 +97,34 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
87
97
|
const ctrlCTimer = useRef(null);
|
|
88
98
|
const runBaselineOut = useRef(0);
|
|
89
99
|
const toastTimers = useRef(new Map());
|
|
100
|
+
/** Every outstanding `gate.request()`. Held in a ref, never in state: it is
|
|
101
|
+
* bookkeeping for promises, and re-rendering on it would be noise. */
|
|
102
|
+
const pendingHuman = useRef(new Set());
|
|
103
|
+
/** Written by `AppShell`'s layout effect, read at wheel-event time (§4.5). */
|
|
104
|
+
const geometryRef = useRef(UNMEASURED_GEOMETRY);
|
|
105
|
+
/**
|
|
106
|
+
* THE SINGLE SETTLE PATH for a human request. Idempotent per entry, and it
|
|
107
|
+
* clears the overlay as its last act.
|
|
108
|
+
*
|
|
109
|
+
* Exactly four callers, and there must be no fifth: the overlay's own submit,
|
|
110
|
+
* `Esc` in `App`, the `ctx.signal` abort listener, and `cancelPending()`.
|
|
111
|
+
* Every one of them can race the others, which is what `done` is for.
|
|
112
|
+
*/
|
|
113
|
+
const settleHuman = useCallback((entry, value) => {
|
|
114
|
+
if (entry.done)
|
|
115
|
+
return;
|
|
116
|
+
entry.done = true;
|
|
117
|
+
pendingHuman.current.delete(entry);
|
|
118
|
+
entry.resolve(value);
|
|
119
|
+
if (pendingHuman.current.size === 0) {
|
|
120
|
+
setHumanRequest(null);
|
|
121
|
+
dispatch({ type: 'setOverlay', overlay: null });
|
|
122
|
+
}
|
|
123
|
+
}, []);
|
|
124
|
+
const cancelPendingHuman = useCallback(() => {
|
|
125
|
+
for (const entry of [...pendingHuman.current])
|
|
126
|
+
settleHuman(entry, null);
|
|
127
|
+
}, [settleHuman]);
|
|
90
128
|
const registry = useMemo(() => {
|
|
91
129
|
const r = new CommandRegistry();
|
|
92
130
|
// Order matters: built-ins first, so `registerSkillCommands` sees them when
|
|
@@ -131,12 +169,31 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
131
169
|
dispatch(action);
|
|
132
170
|
}
|
|
133
171
|
}
|
|
172
|
+
if (event.type !== 'agent_end')
|
|
173
|
+
return;
|
|
174
|
+
// NO OVERLAY OUTLIVES ITS RUN (P1-4). `ctx.signal` already covers the
|
|
175
|
+
// abort path, so this is DELIBERATE REDUNDANCY: it makes "the card cannot
|
|
176
|
+
// survive the run that opened it" a property of the App's lifecycle
|
|
177
|
+
// rather than a consequence of signal plumbing three modules away.
|
|
178
|
+
// Without it, an aborted run leaves a plan card on screen whose `a` key
|
|
179
|
+
// silently flips the session to build, with no visible cause.
|
|
180
|
+
cancelPendingHuman();
|
|
181
|
+
// A deferred `plan -> build` becomes real here and nowhere else (§3.2).
|
|
182
|
+
const applied = controller.applyPendingMode();
|
|
183
|
+
if (applied) {
|
|
184
|
+
dispatch({ type: 'setAgentMode', mode: applied.effective, pending: null });
|
|
185
|
+
dispatch({
|
|
186
|
+
type: 'pushToast',
|
|
187
|
+
level: 'info',
|
|
188
|
+
text: `${MODE_LABEL[applied.effective]} mode.`,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
134
191
|
});
|
|
135
192
|
return () => {
|
|
136
193
|
flushPending();
|
|
137
194
|
unsubscribe();
|
|
138
195
|
};
|
|
139
|
-
}, [controller, flushPending]);
|
|
196
|
+
}, [controller, flushPending, cancelPendingHuman]);
|
|
140
197
|
// --- Elapsed timer + tokens/sec baseline while running. ----------------
|
|
141
198
|
useEffect(() => {
|
|
142
199
|
if (state.status !== 'running') {
|
|
@@ -180,6 +237,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
180
237
|
return undefined;
|
|
181
238
|
return installConsoleBridge((level, text) => dispatch({ type: 'notice', level, text }));
|
|
182
239
|
}, [fullscreen]);
|
|
240
|
+
// `enabled` is `mouseSource`, not `cfg.mouse`: a user whose terminal drops
|
|
241
|
+
// the events must never be shown advice about a mode not in effect (§6.1).
|
|
242
|
+
useStartupNotices(dispatch, {
|
|
243
|
+
enabled: !!mouseSource,
|
|
244
|
+
onSeen: () => persistConfig({ mouseNoticeSeen: true }),
|
|
245
|
+
});
|
|
183
246
|
// --- Exit snapshot: the only way state reaches cli.tsx (§4.4). ----------
|
|
184
247
|
useEffect(() => {
|
|
185
248
|
publishExitSnapshot({
|
|
@@ -205,6 +268,42 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
205
268
|
confirmBridge.handler = null;
|
|
206
269
|
};
|
|
207
270
|
}, [confirmBridge]);
|
|
271
|
+
// --- Human-input bridge (ask_user / submit_plan). ----------------------
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
if (!humanInputBridge)
|
|
274
|
+
return undefined;
|
|
275
|
+
const bridge = humanInputBridge;
|
|
276
|
+
bridge.handler = (req, signal) => new Promise((resolve) => {
|
|
277
|
+
const entry = { resolve, done: false };
|
|
278
|
+
pendingHuman.current.add(entry);
|
|
279
|
+
// THE ONLY CEILING THIS WAIT HAS. `ToolExecutor`'s timeout is
|
|
280
|
+
// cooperative — it aborts the context signal and keeps awaiting the
|
|
281
|
+
// tool — and the idle watchdog is deliberately paused for the duration.
|
|
282
|
+
// Without this listener both ceilings are gone at once and the process
|
|
283
|
+
// wedges behind an overlay nobody is going to answer (P0-2).
|
|
284
|
+
signal?.addEventListener('abort', () => settleHuman(entry, null), { once: true });
|
|
285
|
+
setHumanRequest(req);
|
|
286
|
+
dispatch({
|
|
287
|
+
type: 'setOverlay',
|
|
288
|
+
overlay: req.kind === 'plan' ? 'plan' : 'question',
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
bridge.cancelPending = cancelPendingHuman;
|
|
292
|
+
return () => {
|
|
293
|
+
// BEFORE nulling the handler, not after: `ConfirmBridge` nulls its handler
|
|
294
|
+
// and leaves in-flight promises dangling forever, which with a wizard the
|
|
295
|
+
// user abandons would hang the tool until its ceiling (R-P4).
|
|
296
|
+
cancelPendingHuman();
|
|
297
|
+
bridge.handler = null;
|
|
298
|
+
};
|
|
299
|
+
}, [humanInputBridge, settleHuman, cancelPendingHuman]);
|
|
300
|
+
// --- Seed the mode mirror from its owner, once, on mount. --------------
|
|
301
|
+
// `--plan` / `planModeDefault` are resolved before the App exists, so the
|
|
302
|
+
// reducer's `'build'` seed would render one wrong frame without this.
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
const current = controller.getAgentMode();
|
|
305
|
+
dispatch({ type: 'setAgentMode', mode: current, pending: null });
|
|
306
|
+
}, [controller]);
|
|
208
307
|
// --- Let the controller drive command-list rebuilds. -------------------
|
|
209
308
|
useEffect(() => {
|
|
210
309
|
controller.setOnSkillsChanged(() => setSkillsNonce((n) => n + 1));
|
|
@@ -248,15 +347,44 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
248
347
|
const persistConfig = (patch) => {
|
|
249
348
|
try {
|
|
250
349
|
updatePersistedConfig(patch);
|
|
350
|
+
getLogger().info('config', 'config_write', { keys: Object.keys(patch) });
|
|
251
351
|
}
|
|
252
|
-
catch {
|
|
253
|
-
//
|
|
352
|
+
catch (err) {
|
|
353
|
+
// Still best-effort — a failed persist must not break the live session —
|
|
354
|
+
// but no longer SILENT. Swallowing this is the root of "I changed the
|
|
355
|
+
// model in the TUI and it was back to the old one after a restart", a
|
|
356
|
+
// report with nothing anywhere to investigate.
|
|
357
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
358
|
+
getLogger().error('config', 'config_write_failed', { reason });
|
|
359
|
+
toast('warn', `Could not save settings: ${reason}`);
|
|
254
360
|
}
|
|
255
361
|
};
|
|
256
362
|
const doExit = () => {
|
|
257
363
|
controller.abort();
|
|
258
364
|
exit();
|
|
259
365
|
};
|
|
366
|
+
/**
|
|
367
|
+
* THE SINGLE MODE WRITE PATH (§3.1 / R-P7).
|
|
368
|
+
*
|
|
369
|
+
* `Shift+Tab` and `/plan` both come through here, which is what makes them
|
|
370
|
+
* the same code path rather than two that can drift. Note that the value
|
|
371
|
+
* dispatched into the view is what the controller reports it ADOPTED, never
|
|
372
|
+
* what was requested: dispatching the request is the one way to make the badge
|
|
373
|
+
* claim a permission the gate will not honour.
|
|
374
|
+
*/
|
|
375
|
+
const applyMode = (next, opts = {}) => {
|
|
376
|
+
const applied = controller.setAgentMode(next, opts.force ? { force: true } : {});
|
|
377
|
+
dispatch({ type: 'setAgentMode', mode: applied.effective, pending: applied.pending });
|
|
378
|
+
if (opts.silent)
|
|
379
|
+
return applied;
|
|
380
|
+
if (applied.pending) {
|
|
381
|
+
toast('info', `${MODE_LABEL[applied.pending]} mode applies after this run.`);
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
toast('info', `${MODE_LABEL[applied.effective]} mode.`);
|
|
385
|
+
}
|
|
386
|
+
return applied;
|
|
387
|
+
};
|
|
260
388
|
const makeCtx = (args) => ({
|
|
261
389
|
args,
|
|
262
390
|
controller,
|
|
@@ -269,6 +397,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
269
397
|
exit: doExit,
|
|
270
398
|
submit: (text) => submitMessage(text),
|
|
271
399
|
refreshSkills: () => setSkillsNonce((n) => n + 1),
|
|
400
|
+
applyAgentMode: applyMode,
|
|
272
401
|
});
|
|
273
402
|
const recordPrompt = (text) => {
|
|
274
403
|
const next = [...promptHistory.filter((p) => p !== text), text].slice(-PROMPT_HISTORY_CAP);
|
|
@@ -280,8 +409,9 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
280
409
|
controller.setSubmitCount(nextCount);
|
|
281
410
|
persistConfig({ promptHistory: next, submitCount: nextCount });
|
|
282
411
|
};
|
|
283
|
-
|
|
284
|
-
|
|
412
|
+
/** `repeat` folds a coalesced wheel burst; the keyboard always sends one. */
|
|
413
|
+
const scrollBy = useCallback((kind, repeat = 1) => {
|
|
414
|
+
setScrollIntent((prev) => ({ kind, nonce: (prev?.nonce ?? 0) + 1, repeat }));
|
|
285
415
|
}, []);
|
|
286
416
|
/**
|
|
287
417
|
* Send a message to the agent, bypassing slash parsing.
|
|
@@ -322,8 +452,14 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
322
452
|
controller.setThinkingLevel(values.thinkingLevel);
|
|
323
453
|
controller.setMaxTokens(maxTokens);
|
|
324
454
|
const key = values.apiKey.trim();
|
|
325
|
-
if (key.length > 0)
|
|
455
|
+
if (key.length > 0) {
|
|
456
|
+
// Registration site 4 (§4.4.3), and the one that matters most: this is how
|
|
457
|
+
// a key most often enters the process, and it happens long after
|
|
458
|
+
// `installLogging()` ran. A startup snapshot would miss it entirely, and
|
|
459
|
+
// the vendor regexes only cover keys that look like a vendor's.
|
|
460
|
+
registerSecret(key);
|
|
326
461
|
controller.setApiKey(values.provider, key);
|
|
462
|
+
}
|
|
327
463
|
const patch = {
|
|
328
464
|
provider: values.provider,
|
|
329
465
|
model: values.model,
|
|
@@ -333,7 +469,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
333
469
|
};
|
|
334
470
|
if (key.length > 0)
|
|
335
471
|
patch.apiKeys = { [values.provider]: key };
|
|
472
|
+
// A PARTIAL `log` section, safe only because `updatePersistedConfig` deep-
|
|
473
|
+
// merges it — a shallow merge here would reset `redactSecrets` every time
|
|
474
|
+
// someone touched the level.
|
|
475
|
+
patch.log = { level: values.logLevel };
|
|
336
476
|
persistConfig(patch);
|
|
477
|
+
getLogger().reconfigure({ ...controller.getConfig().log, level: values.logLevel });
|
|
337
478
|
dispatch({ type: 'setOverlay', overlay: null });
|
|
338
479
|
toast('success', 'Settings saved.');
|
|
339
480
|
};
|
|
@@ -347,11 +488,52 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
347
488
|
setConfirmState(null);
|
|
348
489
|
dispatch({ type: 'setOverlay', overlay: null });
|
|
349
490
|
};
|
|
491
|
+
/** Answer every outstanding request with the same response and close up. */
|
|
492
|
+
const resolveHuman = (response) => {
|
|
493
|
+
for (const entry of [...pendingHuman.current])
|
|
494
|
+
settleHuman(entry, response);
|
|
495
|
+
};
|
|
496
|
+
const handlePlanVerdict = (verdict) => {
|
|
497
|
+
if (verdict.decision === 'approved') {
|
|
498
|
+
// BEFORE resolving, not after. `submit_plan` also calls
|
|
499
|
+
// `setAgentMode('build', { force: true })` when its wait returns — that is
|
|
500
|
+
// the authoritative act, and it stays because it must work even if the App
|
|
501
|
+
// is gone by then. But it runs a microtask later, so reading the mode back
|
|
502
|
+
// here would read `'plan'` and the badge would lag by a frame. Writing it
|
|
503
|
+
// first through the same single path makes the tool's call an idempotent
|
|
504
|
+
// no-op and keeps the mirror exact (§3.1 / R-P7).
|
|
505
|
+
applyMode('build', { force: true, silent: true });
|
|
506
|
+
toast('success', 'Plan approved. Implementing.');
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
toast('info', 'Revision requested.');
|
|
510
|
+
}
|
|
511
|
+
resolveHuman({ kind: 'planDecision', decision: verdict.decision, feedback: verdict.feedback });
|
|
512
|
+
};
|
|
513
|
+
// --- Mouse wheel region routing (§4.6). Table and coalescer live in the
|
|
514
|
+
// hook (D-13); only the wiring it cannot own stays here. Inline arrows are
|
|
515
|
+
// fine — the hook re-reads its options through a ref every render. The
|
|
516
|
+
// `Math.max(0, …)` mirrors the keyboard branch below, and for the same
|
|
517
|
+
// reason: `OverlayFrame` only ever reports a clamp DOWNWARD.
|
|
518
|
+
const { clearCoalescer } = useWheelRouting({
|
|
519
|
+
mouseSource,
|
|
520
|
+
geometryRef,
|
|
521
|
+
geometryOverride,
|
|
522
|
+
getOverlay: () => stateRef.current.overlay,
|
|
523
|
+
onScroll: scrollBy,
|
|
524
|
+
onOverlayScroll: (delta) => setOverlayScroll((n) => Math.max(0, n + delta)),
|
|
525
|
+
onHistoryStep: (dir) => setHistoryIntent((prev) => ({ dir, nonce: (prev?.nonce ?? 0) + 1 })),
|
|
526
|
+
});
|
|
350
527
|
// A fresh overlay always opens at the top; carrying the previous overlay's
|
|
351
528
|
// offset over would open Help mid-list for no reason the user can see.
|
|
529
|
+
//
|
|
530
|
+
// `clearCoalescer()` rides along here rather than adding a second subscriber
|
|
531
|
+
// to the same transition (§4.7 rule 2): an accumulated-but-unflushed
|
|
532
|
+
// transcript scroll must not survive an overlay opening.
|
|
352
533
|
useEffect(() => {
|
|
353
534
|
setOverlayScroll(0);
|
|
354
|
-
|
|
535
|
+
clearCoalescer();
|
|
536
|
+
}, [state.overlay, clearCoalescer]);
|
|
355
537
|
// --- Initial overlay / prompt (after handlers are defined). ------------
|
|
356
538
|
useEffect(() => {
|
|
357
539
|
if (initialOverlay)
|
|
@@ -390,7 +572,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
390
572
|
const overlay = stateRef.current.overlay;
|
|
391
573
|
// Modes A only. `model` / `confirm` manage their own keys; registering an
|
|
392
574
|
// arrow handler for them would double every keypress (R-12).
|
|
393
|
-
const controlled = overlay === 'help' || overlay === 'settings';
|
|
575
|
+
const controlled = overlay === 'help' || overlay === 'settings' || overlay === 'plan';
|
|
394
576
|
if (key.pageUp) {
|
|
395
577
|
if (controlled)
|
|
396
578
|
setOverlayScroll((n) => Math.max(0, n - OVERLAY_PAGE));
|
|
@@ -402,12 +584,23 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
402
584
|
return;
|
|
403
585
|
}
|
|
404
586
|
// `settings` keeps Up/Down for field movement — a high-frequency action
|
|
405
|
-
// worth more than scrolling six fields (§4.9).
|
|
406
|
-
|
|
587
|
+
// worth more than scrolling six fields (§4.9). `question` keeps them for
|
|
588
|
+
// its option cursor, for the same reason; `plan` has no cursor of its own
|
|
589
|
+
// and scrolls a line at a time.
|
|
590
|
+
if ((overlay === 'help' || overlay === 'plan') && (key.upArrow || key.downArrow)) {
|
|
407
591
|
setOverlayScroll((n) => Math.max(0, n + (key.upArrow ? -1 : 1)));
|
|
408
592
|
return;
|
|
409
593
|
}
|
|
410
594
|
}
|
|
595
|
+
// AFTER the overlay block, and with an EXPLICIT guard rather than relying on
|
|
596
|
+
// ordering: that block only `return`s for the keys it recognizes, and
|
|
597
|
+
// everything else falls through (AC-P2).
|
|
598
|
+
if (key.tab && key.shift) {
|
|
599
|
+
if (stateRef.current.overlay)
|
|
600
|
+
return;
|
|
601
|
+
applyMode(nextMode(controller.getAgentMode()));
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
411
604
|
if (key.ctrl && (input === 'l' || input === 'L')) {
|
|
412
605
|
if (fullscreen) {
|
|
413
606
|
// Writing \x1B[2J here would erase the screen and then leave it blank:
|
|
@@ -462,6 +655,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
462
655
|
confirmRef.current.resolve(false);
|
|
463
656
|
setConfirmState(null);
|
|
464
657
|
}
|
|
658
|
+
// `App` owns Esc for both plan-mode overlays and they register no
|
|
659
|
+
// handler of their own, so this is the only place a cancellation
|
|
660
|
+
// reaches the waiting tool.
|
|
661
|
+
if (overlay === 'question' || overlay === 'plan')
|
|
662
|
+
cancelPendingHuman();
|
|
663
|
+
setHumanRequest(null);
|
|
465
664
|
dispatch({ type: 'setOverlay', overlay: null });
|
|
466
665
|
return;
|
|
467
666
|
}
|
|
@@ -494,7 +693,14 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
494
693
|
thinkingLevel: cfg.thinkingLevel,
|
|
495
694
|
maxTokens: cfg.maxTokens ? String(cfg.maxTokens) : '',
|
|
496
695
|
apiKey: '',
|
|
497
|
-
|
|
696
|
+
logLevel: cfg.log.level,
|
|
697
|
+
}, apiKeys: cfg.apiKeys, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onSave: handleSettingsSave })) : overlay === 'confirm' && confirmState ? (_jsx(ConfirmDialog, { state: confirmState, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onClose: closeConfirm })) : overlay === 'question' && humanRequest?.kind === 'questions' ? (_jsx(QuestionOverlay, { questions: humanRequest.questions, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onSubmit: (answers) => resolveHuman({ kind: 'answers', answers, cancelled: false }) })) : overlay === 'plan' && humanRequest?.kind === 'plan' ? (_jsx(PlanReviewOverlay, { plan: humanRequest.plan, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onVerdict: handlePlanVerdict })) : null;
|
|
698
|
+
// A MEASUREMENT MUST NEVER OUTLIVE THE FRAME IT DESCRIBED (P1-5): neither
|
|
699
|
+
// path below renders the full-screen `AppShell`, so nothing else would
|
|
700
|
+
// overwrite a stale `bottom`. See `UNMEASURED_GEOMETRY` for why there is no
|
|
701
|
+
// arithmetic fallback.
|
|
702
|
+
if (!fullscreen || rows < MIN_FULLSCREEN_ROWS)
|
|
703
|
+
geometryRef.current = UNMEASURED_GEOMETRY;
|
|
498
704
|
// A resize can drop below the usable floor. Do NOT leave the alt-screen —
|
|
499
705
|
// entering and leaving it on every drag is worse than a placeholder — and take
|
|
500
706
|
// the height from `frameHeight()` like everything else, because this is the
|
|
@@ -512,11 +718,11 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
512
718
|
// the header the moment the first message lands.
|
|
513
719
|
const opener = empty ? (_jsx(SessionOpener, { variant: fullscreen ? pickOpenerVariant(viewportBudget, cols, caps) : 'none', version: version, cwd: controller.getCwd(), hasKey: hasKey, viewportRows: fullscreen ? viewportBudget : Number.POSITIVE_INFINITY, theme: theme, caps: caps })) : null;
|
|
514
720
|
const density = cfg.density;
|
|
515
|
-
const viewport = fullscreen ? (overlayNode ? (_jsx(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: overlayNode })) : (_jsxs(ScrollViewport, { intent: scrollIntent, pinToBottomNonce: pinToBottomNonce, onScrolledLinesChange: setScrolledLines, theme: theme, caps: caps, children: [opener, _jsx(TranscriptList, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: reducedMotion, density: density, theme: theme, caps: caps, windowSize: cfg.transcriptWindow ?? DEFAULT_TRANSCRIPT_WINDOW })] }))) : (_jsxs(_Fragment, { children: [opener, _jsx(Transcript, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: reducedMotion, density: density, theme: theme, caps: caps }), overlayNode] }));
|
|
721
|
+
const viewport = fullscreen ? (overlayNode ? (_jsx(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: overlayNode })) : (_jsxs(ScrollViewport, { intent: scrollIntent, pinToBottomNonce: pinToBottomNonce, onScrolledLinesChange: setScrolledLines, showScrollIndicator: true, theme: theme, caps: caps, children: [opener, _jsx(TranscriptList, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: reducedMotion, density: density, theme: theme, caps: caps, windowSize: cfg.transcriptWindow ?? DEFAULT_TRANSCRIPT_WINDOW })] }))) : (_jsxs(_Fragment, { children: [opener, _jsx(Transcript, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: reducedMotion, density: density, theme: theme, caps: caps }), overlayNode] }));
|
|
516
722
|
// The completion popup lives inside the bottom chrome and can add up to 9
|
|
517
723
|
// rows; unbounded, it pushes the status bar past the frame height where
|
|
518
724
|
// `overflow: hidden` clips it away (R-14 / M-14). Cap it against the viewport.
|
|
519
725
|
const popupMaxRows = fullscreen ? Math.max(1, viewportBudget - 4) : undefined;
|
|
520
|
-
const composer = fullscreen ? (_jsx(Composer, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), showHint: rows >= HINT_MIN_ROWS, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, popupMaxRows: popupMaxRows, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) })) : (_jsx(Box, { marginTop: 1, children: _jsx(PromptInput, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }) }));
|
|
521
|
-
return (_jsx(AppShell, { mode: mode, rows: rows, cols: cols, header: header, viewport: viewport, toast: _jsx(ToastStack, { toasts: state.toasts, theme: theme, mode: mode }), composer: composer, status: _jsx(StatusBar, { model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal, contextTokens: state.contextTokens, contextWindow: modelInfo.contextWindow, contextWindowKnown: modelKnown, status: state.status, elapsedMs: elapsedMs, thinkingLevel: cfg.thinkingLevel, tokPerSec: tokPerSec, theme: theme, caps: caps, scrolledLines: scrolledLines, redrawNonce: redrawNonce }) }));
|
|
726
|
+
const composer = fullscreen ? (_jsx(Composer, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), showHint: rows >= HINT_MIN_ROWS, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, agentMode: state.agentMode, popupMaxRows: popupMaxRows, historyIntent: historyIntent, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) })) : (_jsx(Box, { marginTop: 1, children: _jsx(PromptInput, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), agentMode: state.agentMode, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }) }));
|
|
727
|
+
return (_jsx(AppShell, { mode: mode, rows: rows, cols: cols, geometryRef: geometryRef, header: header, viewport: viewport, toast: _jsx(ToastStack, { toasts: state.toasts, theme: theme, mode: mode }), composer: composer, status: _jsx(StatusBar, { model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal, contextTokens: state.contextTokens, contextWindow: modelInfo.contextWindow, contextWindowKnown: modelKnown, status: state.status, elapsedMs: elapsedMs, thinkingLevel: cfg.thinkingLevel, tokPerSec: tokPerSec, theme: theme, caps: caps, scrolledLines: scrolledLines, redrawNonce: redrawNonce, agentMode: state.agentMode, pendingAgentMode: state.pendingAgentMode }) }));
|
|
522
728
|
}
|