@giovannijecha/jecode 0.8.5 → 0.8.7

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 (104) hide show
  1. package/README.md +36 -9
  2. package/assets/tokenizers/LICENSE +21 -0
  3. package/assets/tokenizers/o200k-base.tiktoken.gz +0 -0
  4. package/dist/cli-info.js +8 -14
  5. package/dist/commands.js +7 -3
  6. package/dist/config.js +47 -53
  7. package/dist/context/automatic.js +16 -1
  8. package/dist/context/budget.js +5 -2
  9. package/dist/context/compactor.js +91 -37
  10. package/dist/context/diagnostics.js +108 -0
  11. package/dist/context/lifetime.js +18 -0
  12. package/dist/context/manager.js +67 -0
  13. package/dist/context/manual.js +11 -10
  14. package/dist/context/measurement.js +75 -0
  15. package/dist/context/policy.js +13 -10
  16. package/dist/context/request-observation.js +46 -0
  17. package/dist/context/request-projection.js +8 -34
  18. package/dist/context/request.js +22 -0
  19. package/dist/context/tokenizer/bpe.js +68 -0
  20. package/dist/context/tokenizer/o200k.js +54 -0
  21. package/dist/context/tokenizer/vocabulary.js +34 -0
  22. package/dist/controller-request.js +47 -45
  23. package/dist/controller.js +13 -2
  24. package/dist/credential-commands.js +3 -3
  25. package/dist/input-boundary.js +0 -62
  26. package/dist/launch.js +13 -9
  27. package/dist/model-command.js +7 -17
  28. package/dist/oauth-result-page.js +68 -0
  29. package/dist/openai-account-command.js +14 -12
  30. package/dist/openai-account.js +7 -5
  31. package/dist/openai-oauth-callback.js +15 -54
  32. package/dist/openai-oauth-tokens.js +9 -7
  33. package/dist/openai-oauth.js +8 -6
  34. package/dist/permission-command.js +9 -24
  35. package/dist/permissions.js +10 -8
  36. package/dist/provider-commands.js +1 -33
  37. package/dist/provider-errors.js +1 -10
  38. package/dist/provider-label.js +3 -10
  39. package/dist/providers/anthropic-wire.js +1 -1
  40. package/dist/providers/anthropic.js +3 -2
  41. package/dist/providers/index.js +1 -5
  42. package/dist/providers/input-measurement.js +85 -0
  43. package/dist/providers/ollama-context.js +42 -0
  44. package/dist/providers/ollama-endpoint.js +7 -34
  45. package/dist/providers/ollama-wire.js +1 -1
  46. package/dist/providers/ollama.js +15 -147
  47. package/dist/providers/openai-codex.js +7 -4
  48. package/dist/providers/openai-stream.js +20 -8
  49. package/dist/providers/openai-summary.js +37 -0
  50. package/dist/providers/openai-wire.js +1 -1
  51. package/dist/providers/openai.js +3 -1
  52. package/dist/sessions/bucket.js +55 -0
  53. package/dist/sessions/catalog-io.js +162 -0
  54. package/dist/sessions/catalog.js +3 -1
  55. package/dist/sessions/codec-messages.js +122 -0
  56. package/dist/sessions/codec-transcript.js +93 -0
  57. package/dist/sessions/codec-values.js +52 -0
  58. package/dist/sessions/codec.js +4 -257
  59. package/dist/sessions/files.js +158 -0
  60. package/dist/sessions/load.js +90 -0
  61. package/dist/sessions/snapshot.js +33 -0
  62. package/dist/sessions/store.js +42 -461
  63. package/dist/settings.js +17 -16
  64. package/dist/start.js +36 -53
  65. package/dist/timeline.js +2 -0
  66. package/dist/tools/file-read.js +192 -0
  67. package/dist/tools/file-summary.js +9 -0
  68. package/dist/tools/{fs.js → file-write.js} +5 -193
  69. package/dist/tools/glob.js +107 -0
  70. package/dist/tools/index.js +2 -1
  71. package/dist/tools/search.js +1 -105
  72. package/dist/tui/activity.js +1 -1
  73. package/dist/tui/app-input.js +34 -17
  74. package/dist/tui/app-workflows.js +13 -361
  75. package/dist/tui/app.js +55 -28
  76. package/dist/tui/approve.js +3 -2
  77. package/dist/tui/blocks.js +1 -2
  78. package/dist/tui/command-workflow.js +112 -0
  79. package/dist/tui/components/command-menu.js +7 -10
  80. package/dist/tui/components/menu.js +82 -43
  81. package/dist/tui/components/messages.js +16 -9
  82. package/dist/tui/components/status.js +1 -1
  83. package/dist/tui/components/tool-evidence.js +107 -0
  84. package/dist/tui/components/tool-motion.js +32 -0
  85. package/dist/tui/components/tool.js +48 -202
  86. package/dist/tui/frame.js +8 -3
  87. package/dist/tui/help.js +3 -2
  88. package/dist/tui/picker-layout.js +44 -0
  89. package/dist/tui/picker.js +7 -71
  90. package/dist/tui/screen.js +7 -0
  91. package/dist/tui/session-view.js +0 -1
  92. package/dist/tui/tool-details.js +135 -0
  93. package/dist/tui/transcript-grammar.js +1 -1
  94. package/dist/tui/transcript-view.js +30 -112
  95. package/dist/tui/turn-workflow.js +235 -0
  96. package/dist/tui/turn.js +7 -140
  97. package/dist/tui/view.js +4 -4
  98. package/dist/tui/workflow-types.js +2 -0
  99. package/dist/ui/render.js +0 -4
  100. package/package.json +16 -12
  101. package/dist/batch-view.js +0 -17
  102. package/dist/batch.js +0 -267
  103. package/dist/ollama-settings-command.js +0 -74
  104. package/dist/tui/motion.js +0 -32
@@ -1,364 +1,16 @@
1
- // Foreground command and model-turn workflows for the TUI shell.
2
- import { handleCommand } from "../commands.js";
3
- import { runTurn } from "../controller.js";
4
- import { resolveContextPolicy } from "../context/capacity.js";
5
- import { automaticCompactionGate, automaticCompactionKey, } from "../context/automatic.js";
6
- import { compactContext } from "../context/compactor.js";
7
- import { compactSession } from "../context/manual.js";
8
- import { isContextOverflow } from "../context/policy.js";
9
- import { updateSettings } from "../settings.js";
10
- import { steeringInbox } from "../steering.js";
11
- import { saveTranscript } from "../transcript-export.js";
12
- import { recordAuxiliaryUsage, recordRequestInput, recordUsage } from "../usage.js";
13
- import { selectTimeline } from "../timeline.js";
14
- import { toolSpecs } from "../tools/index.js";
15
- import { resetRequestIdentity, requestIdentityForSession } from "../request-identity.js";
16
- import { transition } from "./activity.js";
17
- import { answerAt } from "./approve.js";
18
- import * as edit from "./editor.js";
19
- import { cancel as cancelOpen } from "./overlay.js";
20
- import { controllerOptions, turnFailure } from "./session-view.js";
21
- import { transcribe } from "./turn.js";
22
- const WAITING = "Waiting";
1
+ // Wire foreground workflows to the shell and their shared compaction lifetime.
2
+ import { automaticCompactionGate } from "../context/automatic.js";
3
+ import { inputLifetime } from "../context/lifetime.js";
4
+ import { commandWorkflow } from "./command-workflow.js";
5
+ import { turnWorkflow } from "./turn-workflow.js";
23
6
  export function appWorkflows(options) {
24
- const { session, state, permissions, feedback } = options;
25
- let activeSteering;
26
7
  const automaticCompaction = automaticCompactionGate();
27
- const choose = (picker) => new Promise((resolve) => {
28
- state.open = { picker, settle: resolve };
29
- options.render();
30
- });
31
- async function command(text) {
32
- const activity = options.startActivity("command", `Running ${text.split(/\s+/)[0]}`);
33
- if (activity === undefined)
34
- return;
35
- const status = (label) => transition(activity, label);
36
- try {
37
- const outcome = await handleCommand(text, session, {
38
- emit: options.commandNotice,
39
- signal: activity.control.signal,
40
- showHelp: () => new Promise((resolve) => {
41
- state.open = { help: true, settle: resolve };
42
- options.render();
43
- }),
44
- choose,
45
- dismiss: () => {
46
- state.open = state.open === undefined ? undefined : cancelOpen(state.open);
47
- options.render();
48
- },
49
- type: (field) => new Promise((resolve) => {
50
- state.open = { field, settle: resolve };
51
- options.render();
52
- }),
53
- status: (said) => {
54
- status(said ?? activity.label);
55
- options.render();
56
- },
57
- reset: async () => {
58
- await session.persistence?.reset();
59
- resetRequestIdentity(session);
60
- automaticCompaction.reset();
61
- state.blocks.splice(0);
62
- state.past.length = 0;
63
- permissions.reset();
64
- state.scroll = 0;
65
- state.follow = true;
66
- state.unseen = 0;
67
- state.lastMaxScroll = 0;
68
- state.committedNodeId = 0;
69
- },
70
- permissions,
71
- exportTranscript: () => saveTranscript(options.transcriptRoot, state.blocks),
72
- saveSettings: async (patch) => {
73
- await updateSettings(patch);
74
- },
75
- refreshSettings: options.refreshSettings,
76
- timeline: async () => {
77
- const selected = await selectTimeline(session, choose);
78
- if (!selected)
79
- return "unchanged";
80
- options.replaceTranscript();
81
- return session.conversation.activeNodeId === state.committedNodeId
82
- ? "unchanged"
83
- : "selected";
84
- },
85
- compact: async () => {
86
- if (session.conversation.activeNodeId !== state.committedNodeId) {
87
- return "branch-pending";
88
- }
89
- const result = await compactSession(session, {
90
- signal: activity.control.signal,
91
- onStatus: (said) => {
92
- status(said ?? activity.label);
93
- options.render();
94
- },
95
- });
96
- if (result === "compacted")
97
- automaticCompaction.reset();
98
- return result;
99
- },
100
- });
101
- if (outcome === "exit")
102
- state.closeWhenIdle = true;
103
- }
104
- catch (error) {
105
- // Command cancellation is already visible through the dock closing and
106
- // the activity ending. Keep it silent instead of replacing the footer
107
- // with a redundant warning.
108
- if (!activity.control.signal.aborted) {
109
- feedback.show({
110
- text: error.message,
111
- tone: "error",
112
- timeoutMs: 6_000,
113
- });
114
- }
115
- }
116
- finally {
117
- options.finishActivity(activity);
118
- }
119
- }
120
- async function turn(text) {
121
- const activity = options.startActivity("turn", WAITING);
122
- if (activity === undefined)
123
- return;
124
- const inbox = steeringInbox((pending, accepting) => {
125
- if (activeSteering !== inbox)
126
- return;
127
- state.steering = accepting ? pending : undefined;
128
- options.render();
129
- });
130
- activeSteering = inbox;
131
- state.steering = 0;
132
- const status = (label) => transition(activity, label);
133
- const parentId = session.conversation.activeNodeId;
134
- const history = session.conversation.history;
135
- const modelHistory = session.conversation.contextHistory;
136
- const historyStart = history.length;
137
- const blockStart = state.blocks.length;
138
- const createdAt = new Date().toISOString();
139
- const prospectiveNodeId = session.conversation.nodes.length + 1;
140
- let nodeId;
141
- let context;
142
- const unpersistedSteering = [];
143
- const turnTools = permissions.availableTools();
144
- const specs = toolSpecs(turnTools);
145
- let firstPolicy = true;
146
- const policy = () => {
147
- let visible = firstPolicy;
148
- firstPolicy = false;
149
- if (visible) {
150
- status("Checking context");
151
- options.render();
152
- }
153
- return resolveContextPolicy({
154
- provider: session.provider,
155
- model: session.model,
156
- compactionPercent: session.config.compactionPercent,
157
- signal: activity.control.signal,
158
- onStatus: (said) => {
159
- visible = true;
160
- status(said);
161
- options.render();
162
- },
163
- }).finally(() => {
164
- if (visible) {
165
- status(WAITING);
166
- options.render();
167
- }
168
- });
169
- };
170
- options.emit({ kind: "user", text });
171
- const user = { role: "user", content: [{ kind: "text", text }] };
172
- history.push(user);
173
- modelHistory.push(structuredClone(user));
174
- const events = transcribe({
175
- emit: options.emit,
176
- render: options.render,
177
- palette: session.palette,
178
- approved: (call) => permissions.approved(call),
179
- remember: (call) => permissions.remember(call),
180
- ask: (prompt, settle) => {
181
- state.open = { picker: prompt, settle: (index) => settle(answerAt(index)) };
182
- options.render();
183
- },
184
- status: (text) => {
185
- status(text);
186
- },
187
- usage: (usage) => recordUsage(session.usage, usage),
188
- requestInput: (inputTokens) => recordRequestInput(session.usage, inputTokens),
189
- });
190
- const persist = async (checkpoint, settlement, failure) => {
191
- const next = session.conversation.commit({
192
- ...(nodeId === undefined ? {} : { nodeId }),
193
- parentId,
194
- createdAt,
195
- identity: {
196
- providerId: session.provider.id,
197
- model: session.model,
198
- effort: session.config.effort,
199
- },
200
- messages: checkpoint.slice(historyStart),
201
- blocks: state.blocks.slice(blockStart),
202
- ...(context === undefined ? {} : { context }),
203
- ...(failure === undefined ? {} : { failure }),
204
- }, settlement);
205
- await session.persistence?.checkpoint(next);
206
- session.conversation = next;
207
- nodeId = next.activeNodeId;
208
- state.committedNodeId = next.activeNodeId;
209
- unpersistedSteering.length = 0;
210
- };
211
- const compact = async (checkpoint, projected, request) => {
212
- if (request.reason === "overflow" &&
213
- (request.error === undefined || !isContextOverflow(request.error))) {
214
- return undefined;
215
- }
216
- const force = request.reason === "overflow" || request.projectionSaturated;
217
- const key = automaticCompactionKey(session.provider.id, session.model, nodeId ?? prospectiveNodeId, checkpoint.length);
218
- const attempt = { key, reason: request.reason };
219
- if (!automaticCompaction.allows(attempt))
220
- return undefined;
221
- let attempted = false;
222
- const result = await compactContext({
223
- provider: session.provider,
224
- model: session.model,
225
- effort: session.config.effort,
226
- context: projected,
227
- turn: checkpoint.slice(historyStart),
228
- nodeId: nodeId ?? prospectiveNodeId,
229
- coveredMessages: context?.messageCount ?? 0,
230
- lastInputTokens: Math.max(session.usage.lastInputTokens, request.inputTokens),
231
- estimatedInputTokens: request.inputTokens,
232
- signal: activity.control.signal,
233
- force,
234
- policy: request.policy,
235
- requestEnvelope: {
236
- system: session.system,
237
- tools: specs,
238
- maxOutputTokens: session.config.maxTokens,
239
- },
240
- requestIdentity: requestIdentityForSession(session),
241
- onBegin: () => {
242
- attempted = true;
243
- status("Compacting");
244
- options.render();
245
- },
246
- onEnd: () => {
247
- status(WAITING);
248
- options.render();
249
- },
250
- });
251
- if (result === undefined) {
252
- if (attempted && !activity.control.signal.aborted)
253
- automaticCompaction.failed(attempt);
254
- return undefined;
255
- }
256
- automaticCompaction.succeeded(attempt);
257
- context = result.anchor;
258
- if (result.usage !== undefined)
259
- recordAuxiliaryUsage(session.usage, result.usage);
260
- return result.messages;
261
- };
262
- events.onContext = compact;
263
- events.onSteering = (guidance) => {
264
- unpersistedSteering.push(guidance);
265
- options.emit({ kind: "user", text: guidance });
266
- options.render();
267
- };
268
- events.onCheckpoint = async (checkpoint, settlement, projected) => {
269
- await persist(checkpoint, settlement);
270
- const compacted = await compact(checkpoint, projected, {
271
- reason: "budget",
272
- policy: await policy(),
273
- inputTokens: session.usage.lastInputTokens,
274
- projectionSaturated: false,
275
- });
276
- if (compacted !== undefined)
277
- await persist(checkpoint, settlement);
278
- return compacted;
279
- };
280
- let finishReason;
281
- let failed;
282
- try {
283
- await runTurn(history, controllerOptions(session, policy, turnTools, inbox), events, activity.control.signal, modelHistory);
284
- }
285
- catch (error) {
286
- const interrupted = activity.control.signal.aborted;
287
- const completed = nodeId !== undefined && session.conversation.activeNodeId === nodeId &&
288
- session.conversation.activeNode?.settlement === "completed";
289
- if (completed) {
290
- const notice = turnFailure(session, error, interrupted);
291
- feedback.show({ text: notice.text, tone: notice.tone, timeoutMs: 6_000 });
292
- }
293
- else {
294
- finishReason = interrupted ? "interrupted" : "failed";
295
- failed = { error: error, interrupted };
296
- }
297
- }
298
- finally {
299
- let pendingSteering = inbox.close();
300
- if (activeSteering === inbox)
301
- activeSteering = undefined;
302
- state.steering = undefined;
303
- try {
304
- events.finish(finishReason);
305
- if (failed !== undefined) {
306
- const notice = turnFailure(session, failed.error, failed.interrupted);
307
- const settlement = failed.interrupted ? "interrupted" : "failed";
308
- const failure = {
309
- text: notice.text,
310
- tone: failed.interrupted ? "warn" : "error",
311
- };
312
- options.emit(notice);
313
- try {
314
- await persist(closeFailedTurn(history, settlement), settlement, failure);
315
- }
316
- catch (error) {
317
- // A failed persistence boundary cannot remain visible as if it had
318
- // been saved. Revert to the last durable path and return the input
319
- // to the composer so the user can retry without losing it.
320
- options.replaceTranscript();
321
- state.editor = edit.of([
322
- ...(nodeId === undefined ? [text] : []),
323
- ...unpersistedSteering,
324
- ...pendingSteering,
325
- state.editor.text,
326
- ].filter((part) => part !== "").join("\n\n"));
327
- state.completing = undefined;
328
- pendingSteering = [];
329
- feedback.show({
330
- text: error.message,
331
- tone: "error",
332
- timeoutMs: 6_000,
333
- });
334
- }
335
- }
336
- }
337
- finally {
338
- restorePendingSteering(state, pendingSteering);
339
- options.finishActivity(activity);
340
- }
341
- }
342
- }
343
- function steer(text) {
344
- return activeSteering?.offer(text) ?? "unavailable";
345
- }
346
- return { command, turn, steer };
347
- }
348
- function restorePendingSteering(state, messages) {
349
- if (messages.length === 0)
350
- return;
351
- const pending = messages.join("\n\n");
352
- state.editor = edit.of(state.editor.text === "" ? pending : `${pending}\n\n${state.editor.text}`);
353
- state.completing = undefined;
354
- }
355
- function closeFailedTurn(history, settlement) {
356
- const closed = [...history];
357
- if (closed.at(-1)?.role === "assistant")
358
- return closed;
359
- const text = settlement === "interrupted"
360
- ? "The previous attempt was interrupted by the user before completion."
361
- : "The previous attempt failed before completion.";
362
- closed.push({ role: "assistant", content: [{ kind: "text", text }] });
363
- return closed;
8
+ const inputs = inputLifetime();
9
+ return {
10
+ command: commandWorkflow(options, () => {
11
+ automaticCompaction.reset();
12
+ inputs.reset();
13
+ }, () => inputs.reset()),
14
+ ...turnWorkflow(options, automaticCompaction, inputs),
15
+ };
364
16
  }
package/dist/tui/app.js CHANGED
@@ -34,7 +34,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
34
34
  const state = appState();
35
35
  state.blocks.push(...session.conversation.transcript);
36
36
  state.committedNodeId = session.conversation.activeNodeId;
37
- const permissions = sessionPermissions(session.tools, session.config.autoApprove);
37
+ const permissions = sessionPermissions(session.tools);
38
38
  let closed;
39
39
  let frameTimer;
40
40
  let motionTimer;
@@ -42,9 +42,10 @@ export async function runApp(session, transcriptRoot, environment = {}) {
42
42
  let escapeTimer;
43
43
  let stopResize = () => { };
44
44
  let stopInput = () => { };
45
+ let stopOutput = () => { };
45
46
  let stopShutdown = () => { };
46
47
  let failure;
47
- let activeWorkflow;
48
+ const activeWorkflows = new Set();
48
49
  // Timers outlive the teardown they were scheduled before. Painting after the
49
50
  // terminal has been handed back would write escapes into the user's shell.
50
51
  let live = true;
@@ -61,22 +62,25 @@ export async function runApp(session, transcriptRoot, environment = {}) {
61
62
  };
62
63
  const view = () => {
63
64
  const now = Date.now();
65
+ const activity = state.command !== undefined && state.open === undefined && state.approval === undefined
66
+ ? state.command : state.activity ?? state.command;
64
67
  return {
65
68
  blocks: state.blocks,
66
69
  editor: state.editor,
67
70
  scroll: state.scroll,
68
71
  unseen: state.unseen,
69
72
  pal: session.palette,
70
- footer: footerInfo(session, workspace),
71
- status: state.activity === undefined
73
+ footer: state.turnFooter === undefined ? footerInfo(session, workspace) : { ...state.turnFooter, workspace },
74
+ status: activity === undefined
72
75
  ? undefined
73
- : activityStatus(state.activity, now),
74
- steering: state.steering,
76
+ : activityStatus(activity, now),
77
+ turnActive: state.activity !== undefined,
78
+ steering: state.command === undefined ? state.steering : undefined,
75
79
  feedback: state.feedback,
76
80
  readiness: turnBlocker(session),
77
81
  now,
78
82
  reducedMotion: session.config.reducedMotion,
79
- modal: overlay.shown(state.open),
83
+ modal: overlay.shown(state.approval ?? state.open),
80
84
  menu: completionOptions(state.completing),
81
85
  menuIndex: state.completing?.index,
82
86
  };
@@ -143,8 +147,8 @@ export async function runApp(session, transcriptRoot, environment = {}) {
143
147
  if (state.follow)
144
148
  state.unseen = 0;
145
149
  };
146
- const replaceTranscript = () => {
147
- state.blocks.splice(0, state.blocks.length, ...session.conversation.transcript);
150
+ const replaceTranscript = (blocks = session.conversation.transcript) => {
151
+ state.blocks.splice(0, state.blocks.length, ...blocks);
148
152
  state.scroll = 0;
149
153
  state.follow = true;
150
154
  state.unseen = 0;
@@ -158,6 +162,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
158
162
  return;
159
163
  live = false;
160
164
  safely(stopInput);
165
+ safely(stopOutput);
161
166
  safely(stopResize);
162
167
  safely(stopShutdown);
163
168
  if (activityTimer !== undefined)
@@ -183,35 +188,42 @@ export async function runApp(session, transcriptRoot, environment = {}) {
183
188
  function fail(error) {
184
189
  failure ??= { error };
185
190
  state.open = overlay.cancel(state.open);
191
+ state.approval = overlay.cancel(state.approval);
186
192
  state.activity?.control.abort(error);
193
+ state.command?.control.abort(error);
187
194
  quit();
188
195
  }
189
196
  function track(work) {
190
197
  const tracked = work
191
198
  .catch((error) => fail(error))
192
199
  .finally(() => {
193
- if (activeWorkflow === tracked)
194
- activeWorkflow = undefined;
200
+ activeWorkflows.delete(tracked);
195
201
  });
196
- activeWorkflow = tracked;
202
+ activeWorkflows.add(tracked);
197
203
  return tracked;
198
204
  }
199
205
  function requestQuit(reason = new Error("interrupted")) {
200
206
  state.open = overlay.cancel(state.open);
201
- const activity = state.activity;
202
- if (activity === undefined) {
207
+ state.approval = overlay.cancel(state.approval);
208
+ if (state.activity === undefined && state.command === undefined) {
203
209
  quit();
204
210
  return;
205
211
  }
206
212
  state.closeWhenIdle = true;
207
- activity.control.abort(reason);
213
+ state.activity?.control.abort(reason);
214
+ state.command?.control.abort(reason);
208
215
  }
209
216
  function startActivity(kind, label) {
210
- if (state.activity !== undefined)
217
+ if (state.closeWhenIdle)
218
+ return undefined;
219
+ if (state.command !== undefined || (kind === "turn" && state.activity !== undefined))
211
220
  return undefined;
212
221
  const activity = begin(kind, label);
213
- state.activity = activity;
214
- activityTimer = setInterval(() => guard(() => {
222
+ if (kind === "turn")
223
+ state.activity = activity;
224
+ else
225
+ state.command = activity;
226
+ activityTimer ??= setInterval(() => guard(() => {
215
227
  let activeTool;
216
228
  for (let index = state.blocks.length - 1; index >= 0; index--) {
217
229
  const block = state.blocks[index];
@@ -226,14 +238,23 @@ export async function runApp(session, transcriptRoot, environment = {}) {
226
238
  return activity;
227
239
  }
228
240
  function finishActivity(activity) {
229
- if (state.activity !== activity)
241
+ if (state.activity === activity) {
242
+ state.activity = undefined;
243
+ state.turnFooter = undefined;
244
+ state.approval = overlay.cancel(state.approval);
245
+ }
246
+ else if (state.command === activity) {
247
+ state.command = undefined;
248
+ state.open = overlay.cancel(state.open);
249
+ }
250
+ else
230
251
  return;
231
- if (activityTimer !== undefined)
252
+ const idle = state.activity === undefined && state.command === undefined;
253
+ if (idle && activityTimer !== undefined) {
232
254
  clearInterval(activityTimer);
233
- activityTimer = undefined;
234
- state.activity = undefined;
235
- state.open = overlay.cancel(state.open);
236
- if (state.closeWhenIdle)
255
+ activityTimer = undefined;
256
+ }
257
+ if (state.closeWhenIdle && idle)
237
258
  quit();
238
259
  else
239
260
  render();
@@ -273,9 +294,6 @@ export async function runApp(session, transcriptRoot, environment = {}) {
273
294
  invalidate: () => paint.invalidate(),
274
295
  transcriptChanged: (block) => transcript.invalidate(block),
275
296
  });
276
- const resumeAtLaunch = session.resume === undefined
277
- ? undefined
278
- : track(openResumedSession(session.resume));
279
297
  async function openResumedSession(launch) {
280
298
  while (live) {
281
299
  const index = await new Promise((resolve) => {
@@ -314,6 +332,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
314
332
  shutdownSignal?.addEventListener("abort", onShutdown, { once: true });
315
333
  stopShutdown = () => shutdownSignal?.removeEventListener("abort", onShutdown);
316
334
  terminal.enter(session.config.reducedMotion);
335
+ stopOutput = paint.onReady?.(() => guard(() => render())) ?? (() => { });
317
336
  stopResize = terminal.onResize(() => guard(() => {
318
337
  paint.invalidate();
319
338
  render();
@@ -339,6 +358,9 @@ export async function runApp(session, transcriptRoot, environment = {}) {
339
358
  }), ESCAPE_MS);
340
359
  render();
341
360
  }));
361
+ const resumeAtLaunch = session.resume === undefined
362
+ ? undefined
363
+ : track(openResumedSession(session.resume));
342
364
  draw();
343
365
  if (resumeAtLaunch !== undefined)
344
366
  await Promise.race([resumeAtLaunch, done]);
@@ -347,10 +369,15 @@ export async function runApp(session, transcriptRoot, environment = {}) {
347
369
  if (failure !== undefined)
348
370
  throw failure.error;
349
371
  }
372
+ catch (error) {
373
+ // Startup failures must settle pending menus before teardown waits for work.
374
+ fail(error);
375
+ throw error;
376
+ }
350
377
  finally {
351
378
  try {
352
379
  quit();
353
- await activeWorkflow;
380
+ await Promise.allSettled([...activeWorkflows]);
354
381
  }
355
382
  finally {
356
383
  await session.persistence?.close();
@@ -16,13 +16,14 @@ export { scopeFor };
16
16
  export function promptFor(call, target, pal) {
17
17
  const scope = scopeFor(call);
18
18
  const options = [
19
- { label: "Yes, once", hint: "enter", key: "y" },
19
+ { label: "Yes, once", hint: "y", key: "y" },
20
20
  { label: `Yes, ${scopeNoun(scope)} for the session`, hint: "a", key: "a" },
21
- { label: "No, and say why", hint: "esc", key: "n" },
21
+ { label: "No, and say why", hint: "n", key: "n" },
22
22
  ];
23
23
  return {
24
24
  title: [{ text: question(call.name), fg: pal.ink.attention, bold: true }],
25
25
  right: `${call.name}${target === "" ? "" : ` · ${target}`}`,
26
+ controls: "↑↓ choose · enter confirm · esc deny",
26
27
  options,
27
28
  index: 0,
28
29
  };
@@ -14,9 +14,8 @@ export function render(block, width, pal, context = {}) {
14
14
  });
15
15
  case "tool":
16
16
  return renderTool(block, width, pal, {
17
- continues: context.previous?.kind === "tool",
17
+ continues: context.previous?.kind === "reasoning",
18
18
  now: context.now,
19
- motion: context.motion,
20
19
  reducedMotion: context.reducedMotion,
21
20
  });
22
21
  case "notice":