@yeshwanthyk/coding-agent 0.3.12 → 0.3.14

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 (44) hide show
  1. package/dist/adapters/tui/app.js +29 -17
  2. package/dist/adapters/tui/{app.js.map → app.jsx.map} +1 -1
  3. package/dist/components/Footer.js +60 -24
  4. package/dist/components/{Footer.js.map → Footer.jsx.map} +1 -1
  5. package/dist/components/Header.js +312 -96
  6. package/dist/components/{Header.js.map → Header.jsx.map} +1 -1
  7. package/dist/components/MessageList.js +393 -146
  8. package/dist/components/MessageList.jsx.map +1 -0
  9. package/dist/runtime/context.js +15 -9
  10. package/dist/runtime/context.jsx.map +1 -0
  11. package/dist/session-picker.js +123 -69
  12. package/dist/session-picker.jsx.map +1 -0
  13. package/dist/tui-open-rendering.js +716 -343
  14. package/dist/tui-open-rendering.jsx.map +1 -0
  15. package/dist/ui/app-shell/TuiApp.js +590 -441
  16. package/dist/ui/app-shell/{TuiApp.js.map → TuiApp.jsx.map} +1 -1
  17. package/dist/ui/components/modals/ConfirmModal.js +80 -23
  18. package/dist/ui/components/modals/ConfirmModal.jsx.map +1 -0
  19. package/dist/ui/components/modals/EditorModal.js +55 -15
  20. package/dist/ui/components/modals/EditorModal.jsx.map +1 -0
  21. package/dist/ui/components/modals/InputModal.js +36 -9
  22. package/dist/ui/components/modals/InputModal.jsx.map +1 -0
  23. package/dist/ui/components/modals/ModalContainer.js +72 -16
  24. package/dist/ui/components/modals/ModalContainer.jsx.map +1 -0
  25. package/dist/ui/components/modals/SelectModal.js +53 -24
  26. package/dist/ui/components/modals/SelectModal.jsx.map +1 -0
  27. package/dist/ui/features/composer/Composer.js +145 -26
  28. package/dist/ui/features/composer/Composer.jsx.map +1 -0
  29. package/dist/ui/features/main-view/MainView.js +341 -248
  30. package/dist/ui/features/main-view/{MainView.js.map → MainView.jsx.map} +1 -1
  31. package/dist/ui/features/message-pane/MessagePane.js +46 -4
  32. package/dist/ui/features/message-pane/MessagePane.jsx.map +1 -0
  33. package/package.json +2 -2
  34. package/dist/components/MessageList.js.map +0 -1
  35. package/dist/runtime/context.js.map +0 -1
  36. package/dist/session-picker.js.map +0 -1
  37. package/dist/tui-open-rendering.js.map +0 -1
  38. package/dist/ui/components/modals/ConfirmModal.js.map +0 -1
  39. package/dist/ui/components/modals/EditorModal.js.map +0 -1
  40. package/dist/ui/components/modals/InputModal.js.map +0 -1
  41. package/dist/ui/components/modals/ModalContainer.js.map +0 -1
  42. package/dist/ui/components/modals/SelectModal.js.map +0 -1
  43. package/dist/ui/features/composer/Composer.js.map +0 -1
  44. package/dist/ui/features/message-pane/MessagePane.js.map +0 -1
@@ -1,16 +1,15 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "solid-js/h/jsx-runtime";
1
+ import { createComponent as _$createComponent } from "@opentui/solid";
2
2
  import { ThemeProvider } from "@yeshwanthyk/open-tui";
3
3
  import { batch, onMount } from "solid-js";
4
4
  import { Effect } from "effect";
5
5
  /** Detect system dark/light mode (macOS only, defaults to dark) */
6
6
  function detectThemeMode() {
7
- try {
8
- const result = Bun.spawnSync(["defaults", "read", "-g", "AppleInterfaceStyle"]);
9
- return result.stdout.toString().trim().toLowerCase() === "dark" ? "dark" : "light";
10
- }
11
- catch {
12
- return "dark";
13
- }
7
+ try {
8
+ const result = Bun.spawnSync(["defaults", "read", "-g", "AppleInterfaceStyle"]);
9
+ return result.stdout.toString().trim().toLowerCase() === "dark" ? "dark" : "light";
10
+ } catch {
11
+ return "dark";
12
+ }
14
13
  }
15
14
  import { useRuntime } from "../../runtime/context.js";
16
15
  import { createSessionController } from "../../runtime/session/session-controller.js";
@@ -29,450 +28,600 @@ import { completeSimple } from "@yeshwanthyk/ai";
29
28
  import { useModals } from "../hooks/useModals.js";
30
29
  import { ModalContainer } from "../components/modals/ModalContainer.js";
31
30
  const SHELL_INJECTION_PREFIX = "[Shell output]";
32
- export const TuiApp = ({ initialSession }) => {
33
- const runtime = useRuntime();
34
- const { agent, sessionManager, hookRunner, toolByName, customCommands, lsp, config, codexTransport, getApiKey, sendRef, lspActiveRef, cycleModels, validationIssues, } = runtime;
35
- const toolMetaByName = new Map();
36
- for (const [name, entry] of toolByName.entries()) {
37
- toolMetaByName.set(name, {
38
- label: entry.label,
39
- source: entry.source,
40
- sourcePath: entry.sourcePath,
41
- renderCall: entry.renderCall,
42
- renderResult: entry.renderResult,
43
- });
31
+ export const TuiApp = ({
32
+ initialSession
33
+ }) => {
34
+ const runtime = useRuntime();
35
+ const {
36
+ agent,
37
+ sessionManager,
38
+ hookRunner,
39
+ toolByName,
40
+ customCommands,
41
+ lsp,
42
+ config,
43
+ codexTransport,
44
+ getApiKey,
45
+ sendRef,
46
+ lspActiveRef,
47
+ cycleModels,
48
+ validationIssues
49
+ } = runtime;
50
+ const toolMetaByName = new Map();
51
+ for (const [name, entry] of toolByName.entries()) {
52
+ toolMetaByName.set(name, {
53
+ label: entry.label,
54
+ source: entry.source,
55
+ sourcePath: entry.sourcePath,
56
+ renderCall: entry.renderCall,
57
+ renderResult: entry.renderResult
58
+ });
59
+ }
60
+ const store = createAppStore({
61
+ initialTheme: config.theme,
62
+ initialModelId: config.modelId,
63
+ initialThinking: config.thinking,
64
+ initialContextWindow: config.model.contextWindow,
65
+ initialProvider: config.provider
66
+ });
67
+ const promptQueue = createPromptQueue(counts => store.queueCounts.set(counts));
68
+ const modals = useModals();
69
+ const sessionController = createSessionController({
70
+ initialProvider: config.provider,
71
+ initialModel: config.model,
72
+ initialModelId: config.modelId,
73
+ initialThinking: config.thinking,
74
+ agent,
75
+ sessionManager,
76
+ hookRunner,
77
+ toolByName: toolMetaByName,
78
+ setMessages: store.messages.set,
79
+ setContextTokens: store.contextTokens.set,
80
+ setDisplayProvider: store.currentProvider.set,
81
+ setDisplayModelId: store.displayModelId.set,
82
+ setDisplayThinking: store.displayThinking.set,
83
+ setDisplayContextWindow: store.displayContextWindow.set,
84
+ shellInjectionPrefix: SHELL_INJECTION_PREFIX,
85
+ promptQueue
86
+ });
87
+ onMount(() => {
88
+ if (initialSession) {
89
+ sessionController.restoreSession(initialSession);
44
90
  }
45
- const store = createAppStore({
46
- initialTheme: config.theme,
47
- initialModelId: config.modelId,
48
- initialThinking: config.thinking,
49
- initialContextWindow: config.model.contextWindow,
50
- initialProvider: config.provider,
91
+ });
92
+ const ensureSession = () => sessionController.ensureSession();
93
+ let cycleIndex = cycleModels.findIndex(entry => entry.model.id === config.modelId && entry.provider === config.provider);
94
+ if (cycleIndex < 0) cycleIndex = 0;
95
+ const streamingMessageIdRef = {
96
+ current: null
97
+ };
98
+ const retryConfig = {
99
+ enabled: true,
100
+ maxRetries: 3,
101
+ baseDelayMs: 2000
102
+ };
103
+ const retryablePattern = /overloaded|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server error|internal error/i;
104
+ const retryState = {
105
+ attempt: 0,
106
+ abortController: null
107
+ };
108
+ lspActiveRef.setActive = store.lspActive.set;
109
+ const eventCtx = {
110
+ setMessages: store.messages.set,
111
+ setToolBlocks: store.toolBlocks.set,
112
+ setActivityState: store.activityState.set,
113
+ setIsResponding: store.isResponding.set,
114
+ setContextTokens: store.contextTokens.set,
115
+ setCacheStats: store.cacheStats.set,
116
+ setRetryStatus: store.retryStatus.set,
117
+ setTurnCount: store.turnCount.set,
118
+ promptQueue,
119
+ sessionManager,
120
+ streamingMessageId: streamingMessageIdRef,
121
+ retryConfig,
122
+ retryablePattern,
123
+ retryState,
124
+ agent: agent,
125
+ hookRunner,
126
+ toolByName: toolMetaByName,
127
+ getContextWindow: () => store.displayContextWindow.value()
128
+ };
129
+ useAgentEvents({
130
+ agent,
131
+ context: eventCtx
132
+ });
133
+ const handleThemeChange = name => {
134
+ store.theme.set(name);
135
+ void updateAppConfig({
136
+ configDir: config.configDir,
137
+ configPath: config.configPath
138
+ }, {
139
+ theme: name
51
140
  });
52
- const promptQueue = createPromptQueue((counts) => store.queueCounts.set(counts));
53
- const modals = useModals();
54
- const sessionController = createSessionController({
55
- initialProvider: config.provider,
56
- initialModel: config.model,
57
- initialModelId: config.modelId,
58
- initialThinking: config.thinking,
59
- agent,
60
- sessionManager,
61
- hookRunner,
62
- toolByName: toolMetaByName,
63
- setMessages: store.messages.set,
64
- setContextTokens: store.contextTokens.set,
65
- setDisplayProvider: store.currentProvider.set,
66
- setDisplayModelId: store.displayModelId.set,
67
- setDisplayThinking: store.displayThinking.set,
68
- setDisplayContextWindow: store.displayContextWindow.set,
69
- shellInjectionPrefix: SHELL_INJECTION_PREFIX,
70
- promptQueue,
141
+ };
142
+ const exitHandlerRef = {
143
+ current: () => process.exit(0)
144
+ };
145
+ const editorOpenRef = {
146
+ current: async () => {}
147
+ };
148
+ const setEditorTextRef = {
149
+ current: _text => {}
150
+ };
151
+ const getEditorTextRef = {
152
+ current: () => ""
153
+ };
154
+ const showToastRef = {
155
+ current: (_title, _message, _variant) => {}
156
+ };
157
+ // Flag to skip editor clear when hooks populate the editor via setEditorText
158
+ const skipNextEditorClearRef = {
159
+ current: false
160
+ };
161
+ const handleBeforeExit = async () => {
162
+ // Emit shutdown hook before exiting
163
+ await hookRunner.emit({
164
+ type: "session.shutdown",
165
+ sessionId: sessionManager.sessionId
71
166
  });
72
- onMount(() => {
73
- if (initialSession) {
74
- sessionController.restoreSession(initialSession);
75
- }
167
+ };
168
+ const submitPrompt = async (text, mode = "followUp") => {
169
+ const trimmed = text.trim();
170
+ if (!trimmed) return;
171
+ ensureSession();
172
+ let beforeStartResult;
173
+ try {
174
+ beforeStartResult = await hookRunner.emitBeforeAgentStart(trimmed);
175
+ const hookMsg = beforeStartResult?.message ? createHookMessage(beforeStartResult.message) : null;
176
+ if (hookMsg?.display) {
177
+ const uiMsg = {
178
+ id: crypto.randomUUID(),
179
+ role: "assistant",
180
+ content: typeof hookMsg.content === "string" ? hookMsg.content : hookMsg.content.map(p => p.type === "text" ? p.text : "[image]").join(""),
181
+ timestamp: hookMsg.timestamp
182
+ };
183
+ store.messages.set(prev => appendWithCap(prev, uiMsg));
184
+ }
185
+ } catch (err) {
186
+ store.messages.set(prev => appendWithCap(prev, {
187
+ id: crypto.randomUUID(),
188
+ role: "assistant",
189
+ content: `Hook error: ${err instanceof Error ? err.message : String(err)}`,
190
+ timestamp: Date.now()
191
+ }));
192
+ }
193
+ promptQueue.push({
194
+ text: trimmed,
195
+ mode
76
196
  });
77
- const ensureSession = () => sessionController.ensureSession();
78
- let cycleIndex = cycleModels.findIndex((entry) => entry.model.id === config.modelId && entry.provider === config.provider);
79
- if (cycleIndex < 0)
80
- cycleIndex = 0;
81
- const streamingMessageIdRef = { current: null };
82
- const retryConfig = { enabled: true, maxRetries: 3, baseDelayMs: 2000 };
83
- const retryablePattern = /overloaded|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server error|internal error/i;
84
- const retryState = { attempt: 0, abortController: null };
85
- lspActiveRef.setActive = store.lspActive.set;
86
- const eventCtx = {
87
- setMessages: store.messages.set,
88
- setToolBlocks: store.toolBlocks.set,
89
- setActivityState: store.activityState.set,
90
- setIsResponding: store.isResponding.set,
91
- setContextTokens: store.contextTokens.set,
92
- setCacheStats: store.cacheStats.set,
93
- setRetryStatus: store.retryStatus.set,
94
- setTurnCount: store.turnCount.set,
95
- promptQueue,
96
- sessionManager,
97
- streamingMessageId: streamingMessageIdRef,
98
- retryConfig,
99
- retryablePattern,
100
- retryState,
101
- agent: agent,
102
- hookRunner,
103
- toolByName: toolMetaByName,
104
- getContextWindow: () => store.displayContextWindow.value(),
105
- };
106
- useAgentEvents({ agent, context: eventCtx });
107
- const handleThemeChange = (name) => {
108
- store.theme.set(name);
109
- void updateAppConfig({ configDir: config.configDir, configPath: config.configPath }, { theme: name });
110
- };
111
- const exitHandlerRef = { current: () => process.exit(0) };
112
- const editorOpenRef = { current: async () => { } };
113
- const setEditorTextRef = { current: (_text) => { } };
114
- const getEditorTextRef = { current: () => "" };
115
- const showToastRef = { current: (_title, _message, _variant) => { } };
116
- // Flag to skip editor clear when hooks populate the editor via setEditorText
117
- const skipNextEditorClearRef = { current: false };
118
- const handleBeforeExit = async () => {
119
- // Emit shutdown hook before exiting
120
- await hookRunner.emit({ type: "session.shutdown", sessionId: sessionManager.sessionId });
121
- };
122
- const submitPrompt = async (text, mode = "followUp") => {
123
- const trimmed = text.trim();
124
- if (!trimmed)
125
- return;
126
- ensureSession();
127
- let beforeStartResult;
128
- try {
129
- beforeStartResult = await hookRunner.emitBeforeAgentStart(trimmed);
130
- const hookMsg = beforeStartResult?.message ? createHookMessage(beforeStartResult.message) : null;
131
- if (hookMsg?.display) {
132
- const uiMsg = {
133
- id: crypto.randomUUID(),
134
- role: "assistant",
135
- content: typeof hookMsg.content === "string"
136
- ? hookMsg.content
137
- : hookMsg.content.map((p) => (p.type === "text" ? p.text : "[image]")).join(""),
138
- timestamp: hookMsg.timestamp,
139
- };
140
- store.messages.set((prev) => appendWithCap(prev, uiMsg));
141
- }
142
- }
143
- catch (err) {
144
- store.messages.set((prev) => appendWithCap(prev, {
145
- id: crypto.randomUUID(),
146
- role: "assistant",
147
- content: `Hook error: ${err instanceof Error ? err.message : String(err)}`,
148
- timestamp: Date.now(),
149
- }));
197
+ batch(() => {
198
+ store.toolBlocks.set([]);
199
+ store.isResponding.set(true);
200
+ store.activityState.set("thinking");
201
+ });
202
+ try {
203
+ await Effect.runPromise(runtime.sessionOrchestrator.submitPrompt(trimmed, {
204
+ mode,
205
+ beforeStartResult
206
+ }));
207
+ } catch (err) {
208
+ batch(() => {
209
+ store.messages.set(prev => appendWithCap(prev, {
210
+ id: crypto.randomUUID(),
211
+ role: "assistant",
212
+ content: `Error: ${err instanceof Error ? err.message : String(err)}`
213
+ }));
214
+ store.isResponding.set(false);
215
+ store.activityState.set("idle");
216
+ });
217
+ }
218
+ };
219
+ const steerHelper = async text => {
220
+ const trimmed = text.trim();
221
+ if (!trimmed) return;
222
+ if (store.isResponding.value()) {
223
+ await sessionController.steer(trimmed);
224
+ return;
225
+ }
226
+ await submitPrompt(trimmed, "steer");
227
+ };
228
+ const followUpHelper = async text => {
229
+ const trimmed = text.trim();
230
+ if (!trimmed) return;
231
+ if (store.isResponding.value()) {
232
+ await sessionController.followUp(trimmed);
233
+ return;
234
+ }
235
+ await submitPrompt(trimmed, "followUp");
236
+ };
237
+ const sendUserMessageHelper = async (text, options) => {
238
+ const mode = options?.deliverAs ?? "followUp";
239
+ if (mode === "steer") {
240
+ await steerHelper(text);
241
+ return;
242
+ }
243
+ await followUpHelper(text);
244
+ };
245
+ const cmdCtx = {
246
+ agent,
247
+ sessionManager,
248
+ configDir: config.configDir,
249
+ configPath: config.configPath,
250
+ cwd: process.cwd(),
251
+ editor: config.editor,
252
+ codexTransport,
253
+ getApiKey,
254
+ get currentProvider() {
255
+ return sessionController.currentProvider();
256
+ },
257
+ get currentModelId() {
258
+ return sessionController.currentModelId();
259
+ },
260
+ get currentThinking() {
261
+ return sessionController.currentThinking();
262
+ },
263
+ setCurrentProvider: p => sessionController.setCurrentProvider(p),
264
+ setCurrentModelId: id => sessionController.setCurrentModelId(id),
265
+ setCurrentThinking: t => sessionController.setCurrentThinking(t),
266
+ isResponding: store.isResponding.value,
267
+ setIsResponding: store.isResponding.set,
268
+ setActivityState: store.activityState.set,
269
+ setMessages: store.messages.set,
270
+ setToolBlocks: store.toolBlocks.set,
271
+ setContextTokens: store.contextTokens.set,
272
+ setCacheStats: store.cacheStats.set,
273
+ setDisplayModelId: store.displayModelId.set,
274
+ setDisplayThinking: store.displayThinking.set,
275
+ setDisplayContextWindow: store.displayContextWindow.set,
276
+ setDiffWrapMode: store.diffWrapMode.set,
277
+ setConcealMarkdown: store.concealMarkdown.set,
278
+ setTheme: handleThemeChange,
279
+ openEditor: () => editorOpenRef.current(),
280
+ onExit: () => exitHandlerRef.current(),
281
+ hookRunner,
282
+ submitPrompt: (text, options) => submitPrompt(text, options?.mode ?? "followUp"),
283
+ steer: text => steerHelper(text),
284
+ followUp: text => followUpHelper(text),
285
+ sendUserMessage: (text, options) => sendUserMessageHelper(text, options)
286
+ };
287
+ const builtInCommandNames = new Set(slashCommands.map(c => c.name));
288
+ const enqueueWhileResponding = (text, mode) => {
289
+ void sendUserMessageHelper(text, {
290
+ deliverAs: mode
291
+ }).catch(err => {
292
+ store.messages.set(prev => appendWithCap(prev, {
293
+ id: crypto.randomUUID(),
294
+ role: "assistant",
295
+ content: `Error: ${err instanceof Error ? err.message : String(err)}`
296
+ }));
297
+ });
298
+ };
299
+ const handleSubmit = async (text, editorClearFn) => {
300
+ if (!text.trim()) return;
301
+ if (text.startsWith("!")) {
302
+ const shouldInject = text.startsWith("!!");
303
+ const command = text.slice(shouldInject ? 2 : 1).trim();
304
+ if (!command) return;
305
+ editorClearFn?.();
306
+ ensureSession();
307
+ const shellMsgId = crypto.randomUUID();
308
+ const pendingMsg = {
309
+ id: shellMsgId,
310
+ role: "shell",
311
+ command,
312
+ output: "",
313
+ exitCode: null,
314
+ truncated: false,
315
+ timestamp: Date.now()
316
+ };
317
+ store.messages.set(prev => appendWithCap(prev, pendingMsg));
318
+ const result = await runShellCommand(command, {
319
+ timeout: 30000
320
+ });
321
+ const finalMsg = {
322
+ id: shellMsgId,
323
+ role: "shell",
324
+ command,
325
+ output: result.output,
326
+ exitCode: result.exitCode,
327
+ truncated: result.truncated,
328
+ tempFilePath: result.tempFilePath,
329
+ timestamp: Date.now()
330
+ };
331
+ store.messages.set(prev => prev.map(m => m.id === shellMsgId ? finalMsg : m));
332
+ sessionManager.appendMessage({
333
+ role: "shell",
334
+ command,
335
+ output: result.output,
336
+ exitCode: result.exitCode,
337
+ truncated: result.truncated,
338
+ tempFilePath: result.tempFilePath,
339
+ timestamp: Date.now()
340
+ });
341
+ if (shouldInject) {
342
+ const injectionLines = [`${SHELL_INJECTION_PREFIX}`, `$ ${command}`, result.output];
343
+ if (result.exitCode !== null && result.exitCode !== 0) injectionLines.push(`[exit ${result.exitCode}]`);
344
+ if (result.truncated && result.tempFilePath) injectionLines.push(`[truncated, full output: ${result.tempFilePath}]`);
345
+ const injectedText = injectionLines.filter(line => line.length > 0).join("\n");
346
+ const injectionMessage = {
347
+ role: "user",
348
+ content: [{
349
+ type: "text",
350
+ text: injectedText
351
+ }],
352
+ timestamp: Date.now()
353
+ };
354
+ agent.appendMessage(injectionMessage);
355
+ sessionManager.appendMessage(injectionMessage);
356
+ }
357
+ return;
358
+ }
359
+ if (text.startsWith("/")) {
360
+ const trimmed = text.trim();
361
+ const handled = await handleSlashInput(trimmed, {
362
+ commandContext: cmdCtx,
363
+ customCommands,
364
+ builtInCommandNames,
365
+ onExpand: async expanded => handleSubmit(expanded)
366
+ });
367
+ if (handled) {
368
+ // Skip clear if hook populated editor via setEditorText
369
+ if (!skipNextEditorClearRef.current) {
370
+ editorClearFn?.();
150
371
  }
151
- promptQueue.push({ text: trimmed, mode });
152
- batch(() => {
153
- store.toolBlocks.set([]);
154
- store.isResponding.set(true);
155
- store.activityState.set("thinking");
372
+ skipNextEditorClearRef.current = false;
373
+ return;
374
+ }
375
+ }
376
+ if (store.isResponding.value()) {
377
+ enqueueWhileResponding(text, "followUp");
378
+ editorClearFn?.();
379
+ return;
380
+ }
381
+ editorClearFn?.();
382
+ await submitPrompt(text, "followUp");
383
+ };
384
+ // Initialize hook runner with full context
385
+ const hookUIContext = createHookUIContext({
386
+ setEditorText: text => {
387
+ skipNextEditorClearRef.current = true;
388
+ setEditorTextRef.current(text);
389
+ },
390
+ getEditorText: () => getEditorTextRef.current(),
391
+ showSelect: modals.showSelect,
392
+ showInput: modals.showInput,
393
+ showConfirm: modals.showConfirm,
394
+ showEditor: modals.showEditor,
395
+ showNotify: (message, type = "info") => showToastRef.current(type, message, type)
396
+ });
397
+ const hookSessionContext = {
398
+ summarize: async () => {
399
+ // Trigger compaction through the /compact command flow
400
+ await handleSlashInput("/compact", {
401
+ commandContext: cmdCtx,
402
+ customCommands,
403
+ builtInCommandNames,
404
+ onExpand: async expanded => handleSubmit(expanded)
405
+ });
406
+ },
407
+ toast: (title, message, variant = "info") => showToastRef.current(title, message, variant),
408
+ getTokenUsage: () => hookRunner["tokenUsage"],
409
+ getContextLimit: () => hookRunner["contextLimit"],
410
+ newSession: async _opts => {
411
+ // Clear current session and start fresh
412
+ store.messages.set([]);
413
+ store.toolBlocks.set([]);
414
+ store.contextTokens.set(0);
415
+ agent.reset();
416
+ void hookRunner.emit({
417
+ type: "session.clear",
418
+ sessionId: null
419
+ });
420
+ // Start a new session
421
+ sessionManager.startSession(sessionController.currentProvider(), sessionController.currentModelId(), sessionController.currentThinking());
422
+ return {
423
+ cancelled: false,
424
+ sessionId: sessionManager.sessionId ?? undefined
425
+ };
426
+ },
427
+ getApiKey: async model => getApiKey(model.provider),
428
+ complete: async (systemPrompt, userText) => {
429
+ const model = agent.state.model;
430
+ const apiKey = getApiKey(model.provider);
431
+ if (!apiKey) {
432
+ return {
433
+ text: "",
434
+ stopReason: "error"
435
+ };
436
+ }
437
+ try {
438
+ const userMessage = {
439
+ role: "user",
440
+ content: [{
441
+ type: "text",
442
+ text: userText
443
+ }],
444
+ timestamp: Date.now()
445
+ };
446
+ const result = await completeSimple(model, {
447
+ systemPrompt,
448
+ messages: [userMessage]
449
+ }, {
450
+ apiKey
156
451
  });
157
- try {
158
- await Effect.runPromise(runtime.sessionOrchestrator.submitPrompt(trimmed, { mode, beforeStartResult }));
159
- }
160
- catch (err) {
161
- batch(() => {
162
- store.messages.set((prev) => appendWithCap(prev, {
163
- id: crypto.randomUUID(),
164
- role: "assistant",
165
- content: `Error: ${err instanceof Error ? err.message : String(err)}`,
166
- }));
167
- store.isResponding.set(false);
168
- store.activityState.set("idle");
169
- });
170
- }
171
- };
172
- const steerHelper = async (text) => {
173
- const trimmed = text.trim();
174
- if (!trimmed)
175
- return;
176
- if (store.isResponding.value()) {
177
- await sessionController.steer(trimmed);
178
- return;
179
- }
180
- await submitPrompt(trimmed, "steer");
181
- };
182
- const followUpHelper = async (text) => {
183
- const trimmed = text.trim();
184
- if (!trimmed)
185
- return;
186
- if (store.isResponding.value()) {
187
- await sessionController.followUp(trimmed);
188
- return;
189
- }
190
- await submitPrompt(trimmed, "followUp");
191
- };
192
- const sendUserMessageHelper = async (text, options) => {
193
- const mode = options?.deliverAs ?? "followUp";
194
- if (mode === "steer") {
195
- await steerHelper(text);
196
- return;
197
- }
198
- await followUpHelper(text);
199
- };
200
- const cmdCtx = {
201
- agent,
202
- sessionManager,
203
- configDir: config.configDir,
204
- configPath: config.configPath,
205
- cwd: process.cwd(),
206
- editor: config.editor,
207
- codexTransport,
208
- getApiKey,
209
- get currentProvider() {
210
- return sessionController.currentProvider();
452
+ const text = result.content.filter(c => c.type === "text").map(c => c.text).join("\n");
453
+ // Map stopReason: stop -> end, length -> max_tokens, toolUse -> tool_use
454
+ const stopMap = {
455
+ stop: "end",
456
+ length: "max_tokens",
457
+ toolUse: "tool_use",
458
+ error: "error",
459
+ aborted: "aborted"
460
+ };
461
+ return {
462
+ text,
463
+ stopReason: stopMap[result.stopReason] ?? "end"
464
+ };
465
+ } catch (err) {
466
+ return {
467
+ text: err instanceof Error ? err.message : String(err),
468
+ stopReason: "error"
469
+ };
470
+ }
471
+ }
472
+ };
473
+ const sendMessageHandler = (message, triggerTurn) => {
474
+ const hookMessage = createHookMessage(message);
475
+ // Add to UI messages if display is true
476
+ if (hookMessage.display) {
477
+ const uiMsg = {
478
+ id: crypto.randomUUID(),
479
+ role: "assistant",
480
+ // Render hook messages as assistant for now
481
+ content: typeof hookMessage.content === "string" ? hookMessage.content : hookMessage.content.map(p => p.type === "text" ? p.text : "[image]").join(""),
482
+ timestamp: hookMessage.timestamp
483
+ };
484
+ store.messages.set(prev => appendWithCap(prev, uiMsg));
485
+ }
486
+ // Persist hook message to session
487
+ sessionManager.appendMessage(hookMessage);
488
+ // Optionally trigger a new turn
489
+ if (triggerTurn) {
490
+ void handleSubmit(typeof hookMessage.content === "string" ? hookMessage.content : "");
491
+ }
492
+ };
493
+ hookRunner.initialize({
494
+ sendHandler: text => void handleSubmit(text),
495
+ sendMessageHandler,
496
+ sendUserMessageHandler: (text, options) => sendUserMessageHelper(text, options),
497
+ steerHandler: text => steerHelper(text),
498
+ followUpHandler: text => followUpHelper(text),
499
+ isIdleHandler: () => !store.isResponding.value(),
500
+ appendEntryHandler: (customType, data) => sessionManager.appendEntry(customType, data),
501
+ getSessionId: () => sessionManager.sessionId,
502
+ getModel: () => agent.state.model,
503
+ uiContext: hookUIContext,
504
+ sessionContext: hookSessionContext,
505
+ hasUI: true
506
+ });
507
+ sendRef.current = text => void handleSubmit(text);
508
+ const handleAbort = () => {
509
+ if (retryState.abortController) {
510
+ retryState.abortController.abort();
511
+ retryState.abortController = null;
512
+ retryState.attempt = 0;
513
+ store.retryStatus.set(null);
514
+ }
515
+ agent.abort();
516
+ agent.clearMessageQueue();
517
+ const restore = promptQueue.drainToScript();
518
+ Effect.runFork(Effect.catchAll(runtime.sessionOrchestrator.drainToScript, () => Effect.succeed(null)));
519
+ batch(() => {
520
+ store.isResponding.set(false);
521
+ store.activityState.set("idle");
522
+ });
523
+ return restore;
524
+ };
525
+ const cycleModel = () => {
526
+ if (cycleModels.length <= 1) return;
527
+ if (store.isResponding.value()) return;
528
+ cycleIndex = (cycleIndex + 1) % cycleModels.length;
529
+ const entry = cycleModels[cycleIndex];
530
+ sessionController.setCurrentProvider(entry.provider);
531
+ sessionController.setCurrentModelId(entry.model.id);
532
+ agent.setModel(entry.model);
533
+ store.displayModelId.set(entry.model.id);
534
+ store.displayContextWindow.set(entry.model.contextWindow);
535
+ };
536
+ const cycleThinking = () => {
537
+ const current = sessionController.currentThinking();
538
+ const next = THINKING_LEVELS[(THINKING_LEVELS.indexOf(current) + 1) % THINKING_LEVELS.length];
539
+ sessionController.setCurrentThinking(next);
540
+ agent.setThinkingLevel(next);
541
+ store.displayThinking.set(next);
542
+ };
543
+ const themeMode = detectThemeMode();
544
+ return _$createComponent(ThemeProvider, {
545
+ mode: themeMode,
546
+ get themeName() {
547
+ return store.theme.value();
548
+ },
549
+ onThemeChange: handleThemeChange,
550
+ get children() {
551
+ return [_$createComponent(MainView, {
552
+ validationIssues: validationIssues,
553
+ get messages() {
554
+ return store.messages.value();
211
555
  },
212
- get currentModelId() {
213
- return sessionController.currentModelId();
556
+ get toolBlocks() {
557
+ return store.toolBlocks.value();
214
558
  },
215
- get currentThinking() {
216
- return sessionController.currentThinking();
559
+ get isResponding() {
560
+ return store.isResponding.value();
217
561
  },
218
- setCurrentProvider: (p) => sessionController.setCurrentProvider(p),
219
- setCurrentModelId: (id) => sessionController.setCurrentModelId(id),
220
- setCurrentThinking: (t) => sessionController.setCurrentThinking(t),
221
- isResponding: store.isResponding.value,
222
- setIsResponding: store.isResponding.set,
223
- setActivityState: store.activityState.set,
224
- setMessages: store.messages.set,
225
- setToolBlocks: store.toolBlocks.set,
226
- setContextTokens: store.contextTokens.set,
227
- setCacheStats: store.cacheStats.set,
228
- setDisplayModelId: store.displayModelId.set,
229
- setDisplayThinking: store.displayThinking.set,
230
- setDisplayContextWindow: store.displayContextWindow.set,
231
- setDiffWrapMode: store.diffWrapMode.set,
232
- setConcealMarkdown: store.concealMarkdown.set,
233
- setTheme: handleThemeChange,
234
- openEditor: () => editorOpenRef.current(),
235
- onExit: () => exitHandlerRef.current(),
236
- hookRunner,
237
- submitPrompt: (text, options) => submitPrompt(text, options?.mode ?? "followUp"),
238
- steer: (text) => steerHelper(text),
239
- followUp: (text) => followUpHelper(text),
240
- sendUserMessage: (text, options) => sendUserMessageHelper(text, options),
241
- };
242
- const builtInCommandNames = new Set(slashCommands.map((c) => c.name));
243
- const enqueueWhileResponding = (text, mode) => {
244
- void sendUserMessageHelper(text, { deliverAs: mode }).catch((err) => {
245
- store.messages.set((prev) => appendWithCap(prev, {
246
- id: crypto.randomUUID(),
247
- role: "assistant",
248
- content: `Error: ${err instanceof Error ? err.message : String(err)}`,
249
- }));
250
- });
251
- };
252
- const handleSubmit = async (text, editorClearFn) => {
253
- if (!text.trim())
254
- return;
255
- if (text.startsWith("!")) {
256
- const shouldInject = text.startsWith("!!");
257
- const command = text.slice(shouldInject ? 2 : 1).trim();
258
- if (!command)
259
- return;
260
- editorClearFn?.();
261
- ensureSession();
262
- const shellMsgId = crypto.randomUUID();
263
- const pendingMsg = {
264
- id: shellMsgId,
265
- role: "shell",
266
- command,
267
- output: "",
268
- exitCode: null,
269
- truncated: false,
270
- timestamp: Date.now(),
271
- };
272
- store.messages.set((prev) => appendWithCap(prev, pendingMsg));
273
- const result = await runShellCommand(command, { timeout: 30000 });
274
- const finalMsg = {
275
- id: shellMsgId,
276
- role: "shell",
277
- command,
278
- output: result.output,
279
- exitCode: result.exitCode,
280
- truncated: result.truncated,
281
- tempFilePath: result.tempFilePath,
282
- timestamp: Date.now(),
283
- };
284
- store.messages.set((prev) => prev.map((m) => (m.id === shellMsgId ? finalMsg : m)));
285
- sessionManager.appendMessage({
286
- role: "shell",
287
- command,
288
- output: result.output,
289
- exitCode: result.exitCode,
290
- truncated: result.truncated,
291
- tempFilePath: result.tempFilePath,
292
- timestamp: Date.now(),
293
- });
294
- if (shouldInject) {
295
- const injectionLines = [`${SHELL_INJECTION_PREFIX}`, `$ ${command}`, result.output];
296
- if (result.exitCode !== null && result.exitCode !== 0)
297
- injectionLines.push(`[exit ${result.exitCode}]`);
298
- if (result.truncated && result.tempFilePath)
299
- injectionLines.push(`[truncated, full output: ${result.tempFilePath}]`);
300
- const injectedText = injectionLines.filter((line) => line.length > 0).join("\n");
301
- const injectionMessage = {
302
- role: "user",
303
- content: [{ type: "text", text: injectedText }],
304
- timestamp: Date.now(),
305
- };
306
- agent.appendMessage(injectionMessage);
307
- sessionManager.appendMessage(injectionMessage);
308
- }
309
- return;
310
- }
311
- if (text.startsWith("/")) {
312
- const trimmed = text.trim();
313
- const handled = await handleSlashInput(trimmed, {
314
- commandContext: cmdCtx,
315
- customCommands,
316
- builtInCommandNames,
317
- onExpand: async (expanded) => handleSubmit(expanded),
318
- });
319
- if (handled) {
320
- // Skip clear if hook populated editor via setEditorText
321
- if (!skipNextEditorClearRef.current) {
322
- editorClearFn?.();
323
- }
324
- skipNextEditorClearRef.current = false;
325
- return;
326
- }
327
- }
328
- if (store.isResponding.value()) {
329
- enqueueWhileResponding(text, "followUp");
330
- editorClearFn?.();
331
- return;
332
- }
333
- editorClearFn?.();
334
- await submitPrompt(text, "followUp");
335
- };
336
- // Initialize hook runner with full context
337
- const hookUIContext = createHookUIContext({
338
- setEditorText: (text) => {
339
- skipNextEditorClearRef.current = true;
340
- setEditorTextRef.current(text);
562
+ get activityState() {
563
+ return store.activityState.value();
341
564
  },
342
- getEditorText: () => getEditorTextRef.current(),
343
- showSelect: modals.showSelect,
344
- showInput: modals.showInput,
345
- showConfirm: modals.showConfirm,
346
- showEditor: modals.showEditor,
347
- showNotify: (message, type = "info") => showToastRef.current(type, message, type)
348
- });
349
- const hookSessionContext = {
350
- summarize: async () => {
351
- // Trigger compaction through the /compact command flow
352
- await handleSlashInput("/compact", {
353
- commandContext: cmdCtx,
354
- customCommands,
355
- builtInCommandNames,
356
- onExpand: async (expanded) => handleSubmit(expanded),
357
- });
565
+ get thinkingVisible() {
566
+ return store.thinkingVisible.value();
358
567
  },
359
- toast: (title, message, variant = "info") => showToastRef.current(title, message, variant),
360
- getTokenUsage: () => hookRunner["tokenUsage"],
361
- getContextLimit: () => hookRunner["contextLimit"],
362
- newSession: async (_opts) => {
363
- // Clear current session and start fresh
364
- store.messages.set([]);
365
- store.toolBlocks.set([]);
366
- store.contextTokens.set(0);
367
- agent.reset();
368
- void hookRunner.emit({ type: "session.clear", sessionId: null });
369
- // Start a new session
370
- sessionManager.startSession(sessionController.currentProvider(), sessionController.currentModelId(), sessionController.currentThinking());
371
- return { cancelled: false, sessionId: sessionManager.sessionId ?? undefined };
568
+ get modelId() {
569
+ return store.displayModelId.value();
372
570
  },
373
- getApiKey: async (model) => getApiKey(model.provider),
374
- complete: async (systemPrompt, userText) => {
375
- const model = agent.state.model;
376
- const apiKey = getApiKey(model.provider);
377
- if (!apiKey) {
378
- return { text: "", stopReason: "error" };
379
- }
380
- try {
381
- const userMessage = {
382
- role: "user",
383
- content: [{ type: "text", text: userText }],
384
- timestamp: Date.now(),
385
- };
386
- const result = await completeSimple(model, { systemPrompt, messages: [userMessage] }, { apiKey });
387
- const text = result.content
388
- .filter((c) => c.type === "text")
389
- .map((c) => c.text)
390
- .join("\n");
391
- // Map stopReason: stop -> end, length -> max_tokens, toolUse -> tool_use
392
- const stopMap = {
393
- stop: "end", length: "max_tokens", toolUse: "tool_use", error: "error", aborted: "aborted"
394
- };
395
- return { text, stopReason: stopMap[result.stopReason] ?? "end" };
396
- }
397
- catch (err) {
398
- return { text: err instanceof Error ? err.message : String(err), stopReason: "error" };
399
- }
571
+ get thinking() {
572
+ return store.displayThinking.value();
400
573
  },
401
- };
402
- const sendMessageHandler = (message, triggerTurn) => {
403
- const hookMessage = createHookMessage(message);
404
- // Add to UI messages if display is true
405
- if (hookMessage.display) {
406
- const uiMsg = {
407
- id: crypto.randomUUID(),
408
- role: "assistant", // Render hook messages as assistant for now
409
- content: typeof hookMessage.content === "string"
410
- ? hookMessage.content
411
- : hookMessage.content.map(p => p.type === "text" ? p.text : "[image]").join(""),
412
- timestamp: hookMessage.timestamp,
413
- };
414
- store.messages.set((prev) => appendWithCap(prev, uiMsg));
415
- }
416
- // Persist hook message to session
417
- sessionManager.appendMessage(hookMessage);
418
- // Optionally trigger a new turn
419
- if (triggerTurn) {
420
- void handleSubmit(typeof hookMessage.content === "string" ? hookMessage.content : "");
421
- }
422
- };
423
- hookRunner.initialize({
424
- sendHandler: (text) => void handleSubmit(text),
425
- sendMessageHandler,
426
- sendUserMessageHandler: (text, options) => sendUserMessageHelper(text, options),
427
- steerHandler: (text) => steerHelper(text),
428
- followUpHandler: (text) => followUpHelper(text),
429
- isIdleHandler: () => !store.isResponding.value(),
430
- appendEntryHandler: (customType, data) => sessionManager.appendEntry(customType, data),
431
- getSessionId: () => sessionManager.sessionId,
432
- getModel: () => agent.state.model,
433
- uiContext: hookUIContext,
434
- sessionContext: hookSessionContext,
435
- hasUI: true,
436
- });
437
- sendRef.current = (text) => void handleSubmit(text);
438
- const handleAbort = () => {
439
- if (retryState.abortController) {
440
- retryState.abortController.abort();
441
- retryState.abortController = null;
442
- retryState.attempt = 0;
443
- store.retryStatus.set(null);
574
+ get provider() {
575
+ return store.currentProvider.value();
576
+ },
577
+ get contextTokens() {
578
+ return store.contextTokens.value();
579
+ },
580
+ get contextWindow() {
581
+ return store.displayContextWindow.value();
582
+ },
583
+ get queueCounts() {
584
+ return store.queueCounts.value();
585
+ },
586
+ get retryStatus() {
587
+ return store.retryStatus.value();
588
+ },
589
+ get turnCount() {
590
+ return store.turnCount.value();
591
+ },
592
+ get lspActive() {
593
+ return store.lspActive.value();
594
+ },
595
+ get diffWrapMode() {
596
+ return store.diffWrapMode.value();
597
+ },
598
+ get concealMarkdown() {
599
+ return store.concealMarkdown.value();
600
+ },
601
+ customCommands: customCommands,
602
+ onSubmit: handleSubmit,
603
+ onAbort: handleAbort,
604
+ onToggleThinking: () => store.thinkingVisible.set(v => !v),
605
+ onCycleModel: cycleModel,
606
+ onCycleThinking: cycleThinking,
607
+ exitHandlerRef: exitHandlerRef,
608
+ editorOpenRef: editorOpenRef,
609
+ setEditorTextRef: setEditorTextRef,
610
+ getEditorTextRef: getEditorTextRef,
611
+ showToastRef: showToastRef,
612
+ onBeforeExit: handleBeforeExit,
613
+ get editor() {
614
+ return config.editor;
615
+ },
616
+ lsp: lsp
617
+ }), _$createComponent(ModalContainer, {
618
+ get modalState() {
619
+ return modals.modalState();
620
+ },
621
+ get onClose() {
622
+ return modals.closeModal;
444
623
  }
445
- agent.abort();
446
- agent.clearMessageQueue();
447
- const restore = promptQueue.drainToScript();
448
- Effect.runFork(Effect.catchAll(runtime.sessionOrchestrator.drainToScript, () => Effect.succeed(null)));
449
- batch(() => {
450
- store.isResponding.set(false);
451
- store.activityState.set("idle");
452
- });
453
- return restore;
454
- };
455
- const cycleModel = () => {
456
- if (cycleModels.length <= 1)
457
- return;
458
- if (store.isResponding.value())
459
- return;
460
- cycleIndex = (cycleIndex + 1) % cycleModels.length;
461
- const entry = cycleModels[cycleIndex];
462
- sessionController.setCurrentProvider(entry.provider);
463
- sessionController.setCurrentModelId(entry.model.id);
464
- agent.setModel(entry.model);
465
- store.displayModelId.set(entry.model.id);
466
- store.displayContextWindow.set(entry.model.contextWindow);
467
- };
468
- const cycleThinking = () => {
469
- const current = sessionController.currentThinking();
470
- const next = THINKING_LEVELS[(THINKING_LEVELS.indexOf(current) + 1) % THINKING_LEVELS.length];
471
- sessionController.setCurrentThinking(next);
472
- agent.setThinkingLevel(next);
473
- store.displayThinking.set(next);
474
- };
475
- const themeMode = detectThemeMode();
476
- return (_jsxs(ThemeProvider, { mode: themeMode, themeName: store.theme.value(), onThemeChange: handleThemeChange, children: [_jsx(MainView, { validationIssues: validationIssues, messages: store.messages.value(), toolBlocks: store.toolBlocks.value(), isResponding: store.isResponding.value(), activityState: store.activityState.value(), thinkingVisible: store.thinkingVisible.value(), modelId: store.displayModelId.value(), thinking: store.displayThinking.value(), provider: store.currentProvider.value(), contextTokens: store.contextTokens.value(), contextWindow: store.displayContextWindow.value(), queueCounts: store.queueCounts.value(), retryStatus: store.retryStatus.value(), turnCount: store.turnCount.value(), lspActive: store.lspActive.value(), diffWrapMode: store.diffWrapMode.value(), concealMarkdown: store.concealMarkdown.value(), customCommands: customCommands, onSubmit: handleSubmit, onAbort: handleAbort, onToggleThinking: () => store.thinkingVisible.set((v) => !v), onCycleModel: cycleModel, onCycleThinking: cycleThinking, exitHandlerRef: exitHandlerRef, editorOpenRef: editorOpenRef, setEditorTextRef: setEditorTextRef, getEditorTextRef: getEditorTextRef, showToastRef: showToastRef, onBeforeExit: handleBeforeExit, editor: config.editor, lsp: lsp }), _jsx(ModalContainer, { modalState: modals.modalState(), onClose: modals.closeModal })] }));
477
- };
478
- //# sourceMappingURL=TuiApp.js.map
624
+ })];
625
+ }
626
+ });
627
+ };