@dexto/tui 1.13.5 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/agent-backend.cjs +2 -0
  2. package/dist/agent-backend.d.ts +2 -0
  3. package/dist/agent-backend.d.ts.map +1 -1
  4. package/dist/agent-backend.js +2 -0
  5. package/dist/components/ApprovalPrompt.cjs +19 -17
  6. package/dist/components/ApprovalPrompt.d.ts +2 -1
  7. package/dist/components/ApprovalPrompt.d.ts.map +1 -1
  8. package/dist/components/ApprovalPrompt.js +19 -17
  9. package/dist/components/ApprovalPrompt.test.cjs +119 -0
  10. package/dist/components/ApprovalPrompt.test.d.ts +2 -0
  11. package/dist/components/ApprovalPrompt.test.d.ts.map +1 -0
  12. package/dist/components/ApprovalPrompt.test.js +120 -0
  13. package/dist/components/Footer.cjs +5 -1
  14. package/dist/components/Footer.d.ts.map +1 -1
  15. package/dist/components/Footer.js +5 -1
  16. package/dist/components/Footer.test.cjs +47 -0
  17. package/dist/components/Footer.test.d.ts +2 -0
  18. package/dist/components/Footer.test.d.ts.map +1 -0
  19. package/dist/components/Footer.test.js +46 -0
  20. package/dist/components/modes/AlternateBufferCLI.cjs +3 -1
  21. package/dist/components/modes/AlternateBufferCLI.d.ts.map +1 -1
  22. package/dist/components/modes/AlternateBufferCLI.js +3 -1
  23. package/dist/components/modes/StaticCLI.cjs +3 -1
  24. package/dist/components/modes/StaticCLI.d.ts.map +1 -1
  25. package/dist/components/modes/StaticCLI.js +3 -1
  26. package/dist/components/overlays/PermissionsSelector.cjs +139 -0
  27. package/dist/components/overlays/PermissionsSelector.d.ts +16 -0
  28. package/dist/components/overlays/PermissionsSelector.d.ts.map +1 -0
  29. package/dist/components/overlays/PermissionsSelector.js +115 -0
  30. package/dist/components/overlays/PermissionsSelector.test.cjs +105 -0
  31. package/dist/components/overlays/PermissionsSelector.test.d.ts +2 -0
  32. package/dist/components/overlays/PermissionsSelector.test.d.ts.map +1 -0
  33. package/dist/components/overlays/PermissionsSelector.test.js +104 -0
  34. package/dist/containers/InputContainer.cjs +108 -28
  35. package/dist/containers/InputContainer.d.ts +1 -0
  36. package/dist/containers/InputContainer.d.ts.map +1 -1
  37. package/dist/containers/InputContainer.js +108 -28
  38. package/dist/containers/OverlayContainer.cjs +15 -0
  39. package/dist/containers/OverlayContainer.d.ts.map +1 -1
  40. package/dist/containers/OverlayContainer.js +17 -0
  41. package/dist/hooks/useAgentEvents.cjs +6 -3
  42. package/dist/hooks/useAgentEvents.d.ts.map +1 -1
  43. package/dist/hooks/useAgentEvents.js +6 -3
  44. package/dist/hooks/useCLIState.cjs +28 -8
  45. package/dist/hooks/useCLIState.d.ts +1 -0
  46. package/dist/hooks/useCLIState.d.ts.map +1 -1
  47. package/dist/hooks/useCLIState.js +28 -8
  48. package/dist/hooks/useCLIState.test.cjs +583 -0
  49. package/dist/hooks/useCLIState.test.d.ts +2 -0
  50. package/dist/hooks/useCLIState.test.d.ts.map +1 -0
  51. package/dist/hooks/useCLIState.test.js +582 -0
  52. package/dist/index.d.cts +2 -0
  53. package/dist/interactive-commands/commands.test.cjs +6 -0
  54. package/dist/interactive-commands/commands.test.js +6 -0
  55. package/dist/interactive-commands/tool-commands.cjs +7 -0
  56. package/dist/interactive-commands/tool-commands.d.ts.map +1 -1
  57. package/dist/interactive-commands/tool-commands.js +7 -0
  58. package/dist/services/processStream.cjs +4 -2
  59. package/dist/services/processStream.d.ts.map +1 -1
  60. package/dist/services/processStream.js +4 -2
  61. package/dist/services/processStream.test.cjs +63 -0
  62. package/dist/services/processStream.test.js +63 -0
  63. package/dist/state/types.d.ts +1 -1
  64. package/dist/state/types.d.ts.map +1 -1
  65. package/dist/utils/approvalScope.cjs +35 -0
  66. package/dist/utils/approvalScope.d.ts +3 -0
  67. package/dist/utils/approvalScope.d.ts.map +1 -0
  68. package/dist/utils/approvalScope.js +11 -0
  69. package/dist/utils/commandOverlays.cjs +1 -0
  70. package/dist/utils/commandOverlays.d.ts.map +1 -1
  71. package/dist/utils/commandOverlays.js +1 -0
  72. package/package.json +5 -5
@@ -0,0 +1,583 @@
1
+ "use strict";
2
+ var import_react = require("react");
3
+ var import_node_stream = require("node:stream");
4
+ var import_ink = require("ink");
5
+ var import_vitest = require("vitest");
6
+ var import_useCLIState = require("./useCLIState.js");
7
+ var import_KeypressContext = require("../contexts/KeypressContext.js");
8
+ var import_core = require("@dexto/core");
9
+ var import_messageFormatting = require("../utils/messageFormatting.js");
10
+ var import_InputContainer = require("../containers/InputContainer.js");
11
+ import_vitest.vi.mock("../components/input/InputArea.js", () => ({ InputArea: () => null }));
12
+ const agent = {
13
+ getCurrentLLMConfig: () => ({ provider: "openai", model: "gpt-5" }),
14
+ getSessionHistory: async () => [],
15
+ on: () => {
16
+ }
17
+ };
18
+ function mountState(initialSessionId, backend = agent, initialPrompt, withInput = false) {
19
+ const inputRef = (0, import_react.createRef)();
20
+ const stdout = Object.assign(new import_node_stream.PassThrough(), { columns: 120, rows: 40, isTTY: false });
21
+ let state;
22
+ let output = "";
23
+ stdout.on("data", (chunk) => {
24
+ output += chunk.toString();
25
+ });
26
+ function Probe() {
27
+ state = (0, import_useCLIState.useCLIState)({
28
+ agent: backend,
29
+ initialSessionId,
30
+ initialBypassPermissions: true,
31
+ startupInfo: {
32
+ connectedServers: { count: 0, names: [] },
33
+ failedConnections: [],
34
+ toolCount: 0,
35
+ logFile: null
36
+ }
37
+ });
38
+ const transcript = (0, import_react.createElement)(import_ink.Static, {
39
+ items: state.visibleMessages,
40
+ children: (message) => (0, import_react.createElement)(import_ink.Text, { key: message.id }, message.content)
41
+ });
42
+ return (0, import_react.createElement)(
43
+ import_react.Fragment,
44
+ null,
45
+ transcript,
46
+ withInput ? (0, import_react.createElement)(import_InputContainer.InputContainer, {
47
+ ...state,
48
+ ref: inputRef,
49
+ initialPrompt,
50
+ configFilePath: null
51
+ }) : null
52
+ );
53
+ }
54
+ const view = (0, import_ink.render)((0, import_react.createElement)(import_KeypressContext.KeypressProvider, null, (0, import_react.createElement)(Probe)), {
55
+ stdout,
56
+ patchConsole: false,
57
+ exitOnCtrlC: false
58
+ });
59
+ return {
60
+ getState() {
61
+ if (!state) throw new Error("Not mounted");
62
+ return state;
63
+ },
64
+ submit(text) {
65
+ if (!inputRef.current) throw new Error("Input not mounted");
66
+ return inputRef.current.submit(text);
67
+ },
68
+ output() {
69
+ return output;
70
+ },
71
+ close() {
72
+ view.unmount();
73
+ view.cleanup();
74
+ }
75
+ };
76
+ }
77
+ (0, import_vitest.describe)("TUI session permission modes", () => {
78
+ (0, import_vitest.it)("keeps startup auto-approval when the first session is created, then resets on switch", async () => {
79
+ const screen = mountState(null);
80
+ try {
81
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().ui.bypassPermissions).toBe(true));
82
+ screen.getState().setSession((previous) => ({ ...previous, id: "a", hasActiveSession: true }));
83
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().session.id).toBe("a"));
84
+ (0, import_vitest.expect)(screen.getState().ui.bypassPermissions).toBe(true);
85
+ screen.getState().setSession((previous) => ({ ...previous, id: "b", hasActiveSession: true }));
86
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().ui.bypassPermissions).toBe(false));
87
+ } finally {
88
+ screen.close();
89
+ }
90
+ });
91
+ (0, import_vitest.it)("resets accept-edits mode when a conversation is cleared", async () => {
92
+ const screen = mountState("a");
93
+ try {
94
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().session.id).toBe("a"));
95
+ screen.getState().setUi((previous) => ({
96
+ ...previous,
97
+ autoApproveEdits: true,
98
+ bypassPermissions: false
99
+ }));
100
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().ui.autoApproveEdits).toBe(true));
101
+ screen.getState().setSession((previous) => ({ ...previous, id: null, hasActiveSession: false }));
102
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().ui.autoApproveEdits).toBe(false));
103
+ } finally {
104
+ screen.close();
105
+ }
106
+ });
107
+ });
108
+ (0, import_vitest.describe)("TUI resumed history", () => {
109
+ (0, import_vitest.it)("waits for the actual history snapshot before persisting the startup prompt", async () => {
110
+ let releaseHistory = () => {
111
+ };
112
+ const ready = new Promise((resolve) => {
113
+ releaseHistory = resolve;
114
+ });
115
+ const persisted = [
116
+ { role: "user", content: [{ type: "text", text: "earlier task" }] }
117
+ ];
118
+ const stream = import_vitest.vi.fn(async function* (content) {
119
+ persisted.push({ role: "user", content: [{ type: "text", text: content }] });
120
+ yield { name: "run:complete", sessionId: "a", durationMs: 0, totalTokens: 0 };
121
+ });
122
+ const backend = {
123
+ ...agent,
124
+ stream,
125
+ getSessionMetadata: async () => ({ messageCount: 1 }),
126
+ logger: { error: import_vitest.vi.fn() },
127
+ getSessionHistory: import_vitest.vi.fn(async () => {
128
+ await ready;
129
+ return [...persisted];
130
+ })
131
+ };
132
+ const screen = mountState("a", backend, "startup task", true);
133
+ try {
134
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(backend.getSessionHistory).toHaveBeenCalled());
135
+ (0, import_vitest.expect)(stream).not.toHaveBeenCalled();
136
+ screen.getState().buffer.setText("new draft");
137
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().input.value).toBe("new draft"));
138
+ releaseHistory();
139
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(stream).toHaveBeenCalledTimes(1));
140
+ (0, import_vitest.expect)(screen.getState().messages.map((message) => message.content)).toEqual([
141
+ "earlier task",
142
+ "startup task"
143
+ ]);
144
+ (0, import_vitest.expect)(screen.getState().input.value).toBe("new draft");
145
+ (0, import_vitest.expect)(screen.output().match(/startup task/g)).toHaveLength(1);
146
+ } finally {
147
+ releaseHistory();
148
+ screen.close();
149
+ }
150
+ });
151
+ (0, import_vitest.it)("cancels the startup prompt after switching away from its pending resumed session", async () => {
152
+ let releaseHistory = () => {
153
+ };
154
+ const ready = new Promise((resolve) => {
155
+ releaseHistory = resolve;
156
+ });
157
+ const stream = import_vitest.vi.fn(async function* (_content) {
158
+ yield { name: "run:complete", sessionId: "b", durationMs: 0, totalTokens: 0 };
159
+ });
160
+ const backend = {
161
+ ...agent,
162
+ stream,
163
+ getSessionMetadata: async () => ({ messageCount: 1 }),
164
+ logger: { error: import_vitest.vi.fn() },
165
+ getSessionHistory: import_vitest.vi.fn(async () => {
166
+ await ready;
167
+ return [];
168
+ })
169
+ };
170
+ const screen = mountState("a", backend, "startup task for a", true);
171
+ try {
172
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(backend.getSessionHistory).toHaveBeenCalledWith("a"));
173
+ screen.getState().setSession((previous) => ({ ...previous, id: "b" }));
174
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().session.id).toBe("b"));
175
+ (0, import_vitest.expect)(stream).not.toHaveBeenCalled();
176
+ await screen.submit("manual task for b");
177
+ (0, import_vitest.expect)(stream).toHaveBeenCalledExactlyOnceWith("manual task for b", "b");
178
+ releaseHistory();
179
+ screen.getState().setUi((previous) => ({ ...previous, isThinking: true }));
180
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().ui.isThinking).toBe(true));
181
+ (0, import_vitest.expect)(stream).toHaveBeenCalledTimes(1);
182
+ (0, import_vitest.expect)(screen.getState().messages.map((message) => message.content)).toEqual([
183
+ "manual task for b"
184
+ ]);
185
+ } finally {
186
+ releaseHistory();
187
+ screen.close();
188
+ }
189
+ });
190
+ (0, import_vitest.it)("still creates a first session for an ordinary startup prompt", async () => {
191
+ const createSession = import_vitest.vi.fn(async () => ({ id: "created" }));
192
+ const stream = import_vitest.vi.fn(async function* (_content) {
193
+ yield { name: "run:complete", sessionId: "created", durationMs: 0, totalTokens: 0 };
194
+ });
195
+ const backend = {
196
+ ...agent,
197
+ createSession,
198
+ stream,
199
+ getSessionMetadata: async () => ({ messageCount: 1 }),
200
+ logger: { error: import_vitest.vi.fn() }
201
+ };
202
+ const screen = mountState(null, backend, "new startup task", true);
203
+ try {
204
+ await import_vitest.vi.waitFor(
205
+ () => (0, import_vitest.expect)(stream).toHaveBeenCalledExactlyOnceWith("new startup task", "created")
206
+ );
207
+ (0, import_vitest.expect)(createSession).toHaveBeenCalledTimes(1);
208
+ (0, import_vitest.expect)(screen.getState().session.id).toBe("created");
209
+ } finally {
210
+ screen.close();
211
+ }
212
+ });
213
+ (0, import_vitest.it)("preserves multiple submissions and a newer draft while resumed history is pending", async () => {
214
+ let releaseHistory = () => {
215
+ };
216
+ const ready = new Promise((resolve) => {
217
+ releaseHistory = resolve;
218
+ });
219
+ const stream = import_vitest.vi.fn(async function* (_content) {
220
+ yield { name: "run:complete", sessionId: "a", durationMs: 0, totalTokens: 0 };
221
+ });
222
+ const backend = {
223
+ ...agent,
224
+ stream,
225
+ getSessionMetadata: async () => ({ messageCount: 1 }),
226
+ logger: { error: import_vitest.vi.fn() },
227
+ getSessionHistory: import_vitest.vi.fn(async () => {
228
+ await ready;
229
+ return [{ role: "user", content: [{ type: "text", text: "earlier task" }] }];
230
+ })
231
+ };
232
+ const screen = mountState("a", backend, "startup task", true);
233
+ try {
234
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(backend.getSessionHistory).toHaveBeenCalled());
235
+ const first = screen.submit("first task");
236
+ const second = screen.submit("second task");
237
+ const repeated = screen.submit("first task");
238
+ screen.getState().buffer.setText("newer draft");
239
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().input.value).toBe("newer draft"));
240
+ (0, import_vitest.expect)(stream).not.toHaveBeenCalled();
241
+ releaseHistory();
242
+ await Promise.all([first, second, repeated]);
243
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(stream).toHaveBeenCalledTimes(4));
244
+ (0, import_vitest.expect)(stream.mock.calls.map(([content]) => content)).toEqual([
245
+ "startup task",
246
+ "first task",
247
+ "second task",
248
+ "first task"
249
+ ]);
250
+ (0, import_vitest.expect)(screen.getState().input.history).toEqual([
251
+ "earlier task",
252
+ "startup task",
253
+ "first task",
254
+ "second task",
255
+ "first task"
256
+ ]);
257
+ (0, import_vitest.expect)(screen.getState().input.value).toBe("newer draft");
258
+ } finally {
259
+ releaseHistory();
260
+ screen.close();
261
+ }
262
+ });
263
+ (0, import_vitest.it)("releases deferred submission after history failure without hydrating again on return", async () => {
264
+ let releaseHistory = () => {
265
+ };
266
+ const ready = new Promise((resolve) => {
267
+ releaseHistory = resolve;
268
+ });
269
+ const stream = import_vitest.vi.fn(async function* (_content) {
270
+ yield { name: "run:complete", sessionId: "a", durationMs: 0, totalTokens: 0 };
271
+ });
272
+ const backend = {
273
+ ...agent,
274
+ stream,
275
+ getSessionMetadata: async () => ({ messageCount: 1 }),
276
+ logger: { error: import_vitest.vi.fn() },
277
+ getSessionHistory: import_vitest.vi.fn(async () => {
278
+ await ready;
279
+ throw new Error("history unavailable");
280
+ })
281
+ };
282
+ const screen = mountState("a", backend, void 0, true);
283
+ try {
284
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(backend.getSessionHistory).toHaveBeenCalled());
285
+ const pending = screen.submit("continue anyway");
286
+ (0, import_vitest.expect)(stream).not.toHaveBeenCalled();
287
+ releaseHistory();
288
+ await pending;
289
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(stream).toHaveBeenCalledTimes(1));
290
+ (0, import_vitest.expect)(screen.getState().messages.map((message) => message.content)).toEqual([
291
+ "Error: history unavailable",
292
+ "continue anyway"
293
+ ]);
294
+ screen.getState().setSession((previous) => ({ ...previous, id: "b" }));
295
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().session.id).toBe("b"));
296
+ screen.getState().setSession((previous) => ({ ...previous, id: "a" }));
297
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().session.id).toBe("a"));
298
+ (0, import_vitest.expect)(backend.getSessionHistory).toHaveBeenCalledTimes(1);
299
+ } finally {
300
+ releaseHistory();
301
+ screen.close();
302
+ }
303
+ });
304
+ (0, import_vitest.it)("does not send deferred submissions into a different session", async () => {
305
+ let releaseHistory = () => {
306
+ };
307
+ const ready = new Promise((resolve) => {
308
+ releaseHistory = resolve;
309
+ });
310
+ const stream = import_vitest.vi.fn();
311
+ const backend = {
312
+ ...agent,
313
+ stream,
314
+ getSessionHistory: import_vitest.vi.fn(async () => {
315
+ await ready;
316
+ return [];
317
+ })
318
+ };
319
+ const screen = mountState("a", backend, void 0, true);
320
+ try {
321
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(backend.getSessionHistory).toHaveBeenCalled());
322
+ const pending = screen.submit("session a task");
323
+ screen.getState().setSession((previous) => ({ ...previous, id: "b" }));
324
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().session.id).toBe("b"));
325
+ await pending;
326
+ releaseHistory();
327
+ (0, import_vitest.expect)(stream).not.toHaveBeenCalled();
328
+ (0, import_vitest.expect)(screen.getState().messages).toEqual([]);
329
+ } finally {
330
+ releaseHistory();
331
+ screen.close();
332
+ }
333
+ });
334
+ (0, import_vitest.it)("preserves resumed history and a prompt submitted while history is loading", async () => {
335
+ let finishHistory = () => {
336
+ throw new Error("History request not started");
337
+ };
338
+ const history = new Promise((resolve) => {
339
+ finishHistory = resolve;
340
+ });
341
+ const getSessionHistory = import_vitest.vi.fn(() => history);
342
+ const screen = mountState("a", { ...agent, getSessionHistory });
343
+ try {
344
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(getSessionHistory).toHaveBeenCalledWith("a"));
345
+ const prompt = (0, import_messageFormatting.createUserMessage)("continue this task");
346
+ screen.getState().setMessages((previous) => [...previous, prompt]);
347
+ screen.getState().setInput((previous) => ({
348
+ ...previous,
349
+ history: [...previous.history, prompt.content]
350
+ }));
351
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().messages).toEqual([prompt]));
352
+ (0, import_vitest.expect)(screen.output()).not.toContain("continue this task");
353
+ finishHistory([
354
+ { role: "user", content: [{ type: "text", text: "earlier task" }] },
355
+ {
356
+ role: "assistant",
357
+ assistantOutput: { status: "complete" },
358
+ content: [{ type: "text", text: "earlier answer" }]
359
+ }
360
+ ]);
361
+ await import_vitest.vi.waitFor(
362
+ () => (0, import_vitest.expect)(screen.getState().messages.map((message) => message.content)).toEqual([
363
+ "earlier task",
364
+ "earlier answer",
365
+ "continue this task"
366
+ ])
367
+ );
368
+ (0, import_vitest.expect)(screen.getState().input.history).toEqual(["earlier task", "continue this task"]);
369
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.output()).toContain("continue this task"));
370
+ (0, import_vitest.expect)(screen.output().indexOf("earlier task")).toBeLessThan(
371
+ screen.output().indexOf("continue this task")
372
+ );
373
+ (0, import_vitest.expect)(screen.output().indexOf("earlier answer")).toBeLessThan(
374
+ screen.output().indexOf("continue this task")
375
+ );
376
+ (0, import_vitest.expect)(screen.output().match(/continue this task/g)).toHaveLength(1);
377
+ (0, import_vitest.expect)(getSessionHistory).toHaveBeenCalledTimes(1);
378
+ } finally {
379
+ screen.close();
380
+ }
381
+ });
382
+ (0, import_vitest.it)("discards pending resumed history after switching to another session", async () => {
383
+ let finishHistory = () => {
384
+ throw new Error("History request not started");
385
+ };
386
+ const history = new Promise((resolve) => {
387
+ finishHistory = resolve;
388
+ });
389
+ const getSessionHistory = import_vitest.vi.fn(() => history);
390
+ const screen = mountState("a", { ...agent, getSessionHistory });
391
+ try {
392
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(getSessionHistory).toHaveBeenCalledWith("a"));
393
+ screen.getState().setSession((previous) => ({
394
+ ...previous,
395
+ id: "b",
396
+ hasActiveSession: true
397
+ }));
398
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().session.id).toBe("b"));
399
+ const prompt = (0, import_messageFormatting.createUserMessage)("session b prompt");
400
+ screen.getState().setMessages([prompt]);
401
+ screen.getState().setInput((previous) => ({ ...previous, history: [prompt.content] }));
402
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.output()).toContain("session b prompt"));
403
+ finishHistory([
404
+ { role: "user", content: [{ type: "text", text: "session a history" }] }
405
+ ]);
406
+ await history;
407
+ screen.getState().setUi((previous) => ({ ...previous, isThinking: true }));
408
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().ui.isThinking).toBe(true));
409
+ (0, import_vitest.expect)(screen.getState().messages).toEqual([prompt]);
410
+ (0, import_vitest.expect)(screen.getState().input.history).toEqual(["session b prompt"]);
411
+ (0, import_vitest.expect)(screen.output()).not.toContain("session a history");
412
+ (0, import_vitest.expect)(getSessionHistory).toHaveBeenCalledTimes(1);
413
+ } finally {
414
+ screen.close();
415
+ }
416
+ });
417
+ (0, import_vitest.it)("loads ordinary resumed history once without replacing a current input draft", async () => {
418
+ const getSessionHistory = import_vitest.vi.fn(
419
+ async () => [
420
+ { role: "user", content: [{ type: "text", text: "earlier task" }] },
421
+ {
422
+ role: "assistant",
423
+ assistantOutput: { status: "complete" },
424
+ content: [{ type: "text", text: "earlier answer" }]
425
+ }
426
+ ]
427
+ );
428
+ const screen = mountState("a", { ...agent, getSessionHistory });
429
+ try {
430
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().messages).toHaveLength(2));
431
+ screen.getState().buffer.setText("next task draft");
432
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().input.value).toBe("next task draft"));
433
+ (0, import_vitest.expect)(screen.getState().input.history).toEqual(["earlier task"]);
434
+ screen.getState().setSession((previous) => ({ ...previous, id: "b" }));
435
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().session.id).toBe("b"));
436
+ screen.getState().setSession((previous) => ({ ...previous, id: "a" }));
437
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().session.id).toBe("a"));
438
+ (0, import_vitest.expect)(screen.getState().messages.map((message) => message.content)).toEqual([
439
+ "earlier task",
440
+ "earlier answer"
441
+ ]);
442
+ (0, import_vitest.expect)(getSessionHistory).toHaveBeenCalledTimes(1);
443
+ } finally {
444
+ screen.close();
445
+ }
446
+ });
447
+ (0, import_vitest.it)("releases submitted messages to static output when resumed history fails", async () => {
448
+ let releaseHistory = () => {
449
+ };
450
+ const ready = new Promise((resolve) => {
451
+ releaseHistory = resolve;
452
+ });
453
+ const getSessionHistory = import_vitest.vi.fn(async () => {
454
+ await ready;
455
+ throw new Error("history unavailable");
456
+ });
457
+ const screen = mountState("a", { ...agent, getSessionHistory });
458
+ try {
459
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(getSessionHistory).toHaveBeenCalledWith("a"));
460
+ const prompt = (0, import_messageFormatting.createUserMessage)("continue without history");
461
+ screen.getState().setMessages([prompt]);
462
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().messages).toEqual([prompt]));
463
+ (0, import_vitest.expect)(screen.output()).not.toContain(prompt.content);
464
+ releaseHistory();
465
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.output()).toContain("Error: history unavailable"));
466
+ (0, import_vitest.expect)(screen.output()).toContain(prompt.content);
467
+ (0, import_vitest.expect)(screen.output().match(/continue without history/g)).toHaveLength(1);
468
+ (0, import_vitest.expect)(screen.getState().messages.map((message) => message.content)).toEqual([
469
+ prompt.content,
470
+ "Error: history unavailable"
471
+ ]);
472
+ } finally {
473
+ screen.close();
474
+ }
475
+ });
476
+ });
477
+ (0, import_vitest.describe)("TUI session queue isolation", () => {
478
+ (0, import_vitest.it)("keeps a current-session queue snapshot while the composer changes", async () => {
479
+ const events = new import_core.AgentEventBus();
480
+ let release = () => {
481
+ };
482
+ const pending = new Promise((resolve) => {
483
+ release = resolve;
484
+ });
485
+ const messages = [
486
+ { id: "queued", content: [{ type: "text", text: "queued task" }], queuedAt: 1 }
487
+ ];
488
+ const backend = {
489
+ ...agent,
490
+ on: events.on.bind(events),
491
+ getSteerMessages: import_vitest.vi.fn(async () => pending)
492
+ };
493
+ const screen = mountState("a", backend);
494
+ try {
495
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().session.id).toBe("a"));
496
+ events.emit("message:queued", {
497
+ sessionId: "a",
498
+ queue: "steer",
499
+ id: "queued",
500
+ position: 1
501
+ });
502
+ screen.getState().buffer.setText("a newer draft");
503
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().input.value).toBe("a newer draft"));
504
+ release(messages);
505
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().steerMessages).toEqual(messages));
506
+ (0, import_vitest.expect)(screen.getState().input.value).toBe("a newer draft");
507
+ } finally {
508
+ release([]);
509
+ screen.close();
510
+ }
511
+ });
512
+ import_vitest.it.each(["steer", "follow-up"])(
513
+ "ignores %s queue events from another session",
514
+ async (queue) => {
515
+ const events = new import_core.AgentEventBus();
516
+ const getMessages = import_vitest.vi.fn(async () => []);
517
+ const backend = {
518
+ ...agent,
519
+ on: events.on.bind(events),
520
+ getSteerMessages: getMessages,
521
+ getFollowUpMessages: getMessages
522
+ };
523
+ const screen = mountState("a", backend);
524
+ try {
525
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().session.id).toBe("a"));
526
+ events.emit("message:queued", { sessionId: "b", queue, id: "other", position: 1 });
527
+ events.emit("message:removed", { sessionId: "b", queue, id: "other" });
528
+ (0, import_vitest.expect)(getMessages).not.toHaveBeenCalled();
529
+ } finally {
530
+ screen.close();
531
+ }
532
+ }
533
+ );
534
+ import_vitest.it.each(["steer", "follow-up"])(
535
+ "discards a pending %s snapshot after switching sessions",
536
+ async (queue) => {
537
+ const events = new import_core.AgentEventBus();
538
+ let release = () => {
539
+ };
540
+ const pending = new Promise((resolve) => {
541
+ release = resolve;
542
+ });
543
+ const current = [
544
+ { id: "current", content: [{ type: "text", text: "current task" }], queuedAt: 1 }
545
+ ];
546
+ const getMessages = import_vitest.vi.fn(
547
+ async (sessionId) => sessionId === "a" ? pending : current
548
+ );
549
+ const backend = {
550
+ ...agent,
551
+ on: events.on.bind(events),
552
+ getSteerMessages: getMessages,
553
+ getFollowUpMessages: getMessages
554
+ };
555
+ const screen = mountState("a", backend);
556
+ const visibleQueue = () => queue === "steer" ? screen.getState().steerMessages : screen.getState().queuedMessages;
557
+ try {
558
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().session.id).toBe("a"));
559
+ events.emit("message:queued", { sessionId: "a", queue, id: "old", position: 1 });
560
+ (0, import_vitest.expect)(getMessages).toHaveBeenCalledWith("a");
561
+ screen.getState().setSession((previous) => ({ ...previous, id: "b" }));
562
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().session.id).toBe("b"));
563
+ events.emit("message:queued", {
564
+ sessionId: "b",
565
+ queue,
566
+ id: "current",
567
+ position: 1
568
+ });
569
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(visibleQueue()).toEqual(current));
570
+ release([
571
+ { id: "old", content: [{ type: "text", text: "old task" }], queuedAt: 0 }
572
+ ]);
573
+ await pending;
574
+ screen.getState().setUi((previous) => ({ ...previous, isThinking: true }));
575
+ await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(screen.getState().ui.isThinking).toBe(true));
576
+ (0, import_vitest.expect)(visibleQueue()).toEqual(current);
577
+ } finally {
578
+ release([]);
579
+ screen.close();
580
+ }
581
+ }
582
+ );
583
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=useCLIState.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCLIState.test.d.ts","sourceRoot":"","sources":["../../src/hooks/useCLIState.test.ts"],"names":[],"mappings":""}