zoe-agent 0.3.1

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 (267) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/LICENSE +96 -0
  3. package/README.md +568 -0
  4. package/dist/adapters/cli/agent.d.ts +59 -0
  5. package/dist/adapters/cli/agent.js +232 -0
  6. package/dist/adapters/cli/bootstrap.d.ts +25 -0
  7. package/dist/adapters/cli/bootstrap.js +204 -0
  8. package/dist/adapters/cli/commands/build-registry.d.ts +14 -0
  9. package/dist/adapters/cli/commands/build-registry.js +88 -0
  10. package/dist/adapters/cli/commands/clear.d.ts +7 -0
  11. package/dist/adapters/cli/commands/clear.js +10 -0
  12. package/dist/adapters/cli/commands/compact.d.ts +13 -0
  13. package/dist/adapters/cli/commands/compact.js +96 -0
  14. package/dist/adapters/cli/commands/exit.d.ts +7 -0
  15. package/dist/adapters/cli/commands/exit.js +9 -0
  16. package/dist/adapters/cli/commands/gateway.d.ts +7 -0
  17. package/dist/adapters/cli/commands/gateway.js +152 -0
  18. package/dist/adapters/cli/commands/help.d.ts +9 -0
  19. package/dist/adapters/cli/commands/help.js +12 -0
  20. package/dist/adapters/cli/commands/models.d.ts +10 -0
  21. package/dist/adapters/cli/commands/models.js +32 -0
  22. package/dist/adapters/cli/commands/registry.d.ts +70 -0
  23. package/dist/adapters/cli/commands/registry.js +111 -0
  24. package/dist/adapters/cli/commands/settings-utils.d.ts +38 -0
  25. package/dist/adapters/cli/commands/settings-utils.js +182 -0
  26. package/dist/adapters/cli/commands/settings.d.ts +9 -0
  27. package/dist/adapters/cli/commands/settings.js +395 -0
  28. package/dist/adapters/cli/commands/skills.d.ts +7 -0
  29. package/dist/adapters/cli/commands/skills.js +21 -0
  30. package/dist/adapters/cli/config-loader.d.ts +27 -0
  31. package/dist/adapters/cli/config-loader.js +48 -0
  32. package/dist/adapters/cli/docker-utils.d.ts +37 -0
  33. package/dist/adapters/cli/docker-utils.js +90 -0
  34. package/dist/adapters/cli/index.d.ts +2 -0
  35. package/dist/adapters/cli/index.js +88 -0
  36. package/dist/adapters/cli/repl.d.ts +22 -0
  37. package/dist/adapters/cli/repl.js +256 -0
  38. package/dist/adapters/cli/setup.d.ts +19 -0
  39. package/dist/adapters/cli/setup.js +613 -0
  40. package/dist/adapters/cli/system-prompts.d.ts +56 -0
  41. package/dist/adapters/cli/system-prompts.js +131 -0
  42. package/dist/adapters/cli/tui/app.d.ts +58 -0
  43. package/dist/adapters/cli/tui/app.js +314 -0
  44. package/dist/adapters/cli/tui/components/assistant-message.d.ts +5 -0
  45. package/dist/adapters/cli/tui/components/assistant-message.js +9 -0
  46. package/dist/adapters/cli/tui/components/autocomplete.d.ts +19 -0
  47. package/dist/adapters/cli/tui/components/autocomplete.js +75 -0
  48. package/dist/adapters/cli/tui/components/command-palette.d.ts +15 -0
  49. package/dist/adapters/cli/tui/components/command-palette.js +50 -0
  50. package/dist/adapters/cli/tui/components/diff-viewer.d.ts +5 -0
  51. package/dist/adapters/cli/tui/components/diff-viewer.js +109 -0
  52. package/dist/adapters/cli/tui/components/error-message.d.ts +5 -0
  53. package/dist/adapters/cli/tui/components/error-message.js +8 -0
  54. package/dist/adapters/cli/tui/components/footer.d.ts +20 -0
  55. package/dist/adapters/cli/tui/components/footer.js +19 -0
  56. package/dist/adapters/cli/tui/components/goal-status.d.ts +12 -0
  57. package/dist/adapters/cli/tui/components/goal-status.js +22 -0
  58. package/dist/adapters/cli/tui/components/info-message.d.ts +5 -0
  59. package/dist/adapters/cli/tui/components/info-message.js +8 -0
  60. package/dist/adapters/cli/tui/components/logo-banner.d.ts +7 -0
  61. package/dist/adapters/cli/tui/components/logo-banner.js +33 -0
  62. package/dist/adapters/cli/tui/components/markdown.d.ts +9 -0
  63. package/dist/adapters/cli/tui/components/markdown.js +92 -0
  64. package/dist/adapters/cli/tui/components/message-area.d.ts +19 -0
  65. package/dist/adapters/cli/tui/components/message-area.js +55 -0
  66. package/dist/adapters/cli/tui/components/permission-prompt.d.ts +13 -0
  67. package/dist/adapters/cli/tui/components/permission-prompt.js +32 -0
  68. package/dist/adapters/cli/tui/components/prompt-area.d.ts +22 -0
  69. package/dist/adapters/cli/tui/components/prompt-area.js +68 -0
  70. package/dist/adapters/cli/tui/components/text-input.d.ts +27 -0
  71. package/dist/adapters/cli/tui/components/text-input.js +142 -0
  72. package/dist/adapters/cli/tui/components/tool-call-block.d.ts +11 -0
  73. package/dist/adapters/cli/tui/components/tool-call-block.js +68 -0
  74. package/dist/adapters/cli/tui/components/user-message.d.ts +5 -0
  75. package/dist/adapters/cli/tui/components/user-message.js +8 -0
  76. package/dist/adapters/cli/tui/diff/file-write-meta.d.ts +11 -0
  77. package/dist/adapters/cli/tui/diff/file-write-meta.js +11 -0
  78. package/dist/adapters/cli/tui/diff/line-diff.d.ts +17 -0
  79. package/dist/adapters/cli/tui/diff/line-diff.js +44 -0
  80. package/dist/adapters/cli/tui/feed-serializer.d.ts +29 -0
  81. package/dist/adapters/cli/tui/feed-serializer.js +70 -0
  82. package/dist/adapters/cli/tui/file-index.d.ts +8 -0
  83. package/dist/adapters/cli/tui/file-index.js +41 -0
  84. package/dist/adapters/cli/tui/hooks/use-agent.d.ts +54 -0
  85. package/dist/adapters/cli/tui/hooks/use-agent.js +177 -0
  86. package/dist/adapters/cli/tui/hooks/use-feed.d.ts +16 -0
  87. package/dist/adapters/cli/tui/hooks/use-feed.js +25 -0
  88. package/dist/adapters/cli/tui/hooks/use-file-watcher.d.ts +10 -0
  89. package/dist/adapters/cli/tui/hooks/use-file-watcher.js +43 -0
  90. package/dist/adapters/cli/tui/hooks/use-keybindings.d.ts +16 -0
  91. package/dist/adapters/cli/tui/hooks/use-keybindings.js +25 -0
  92. package/dist/adapters/cli/tui/hooks/use-theme.d.ts +8 -0
  93. package/dist/adapters/cli/tui/hooks/use-theme.js +12 -0
  94. package/dist/adapters/cli/tui/index.d.ts +19 -0
  95. package/dist/adapters/cli/tui/index.js +206 -0
  96. package/dist/adapters/cli/tui/ink-reset.d.ts +29 -0
  97. package/dist/adapters/cli/tui/ink-reset.js +57 -0
  98. package/dist/adapters/cli/tui/layout.d.ts +15 -0
  99. package/dist/adapters/cli/tui/layout.js +15 -0
  100. package/dist/adapters/cli/tui/logo/gradient.d.ts +11 -0
  101. package/dist/adapters/cli/tui/logo/gradient.js +31 -0
  102. package/dist/adapters/cli/tui/overlays/help-dialog.d.ts +4 -0
  103. package/dist/adapters/cli/tui/overlays/help-dialog.js +26 -0
  104. package/dist/adapters/cli/tui/overlays/model-selector.d.ts +14 -0
  105. package/dist/adapters/cli/tui/overlays/model-selector.js +43 -0
  106. package/dist/adapters/cli/tui/overlays/session-selector.d.ts +35 -0
  107. package/dist/adapters/cli/tui/overlays/session-selector.js +162 -0
  108. package/dist/adapters/cli/tui/overlays/settings-overlay.d.ts +24 -0
  109. package/dist/adapters/cli/tui/overlays/settings-overlay.js +126 -0
  110. package/dist/adapters/cli/tui/session-export.d.ts +21 -0
  111. package/dist/adapters/cli/tui/session-export.js +63 -0
  112. package/dist/adapters/cli/tui/theme.d.ts +23 -0
  113. package/dist/adapters/cli/tui/theme.js +22 -0
  114. package/dist/adapters/cli/tui/types.d.ts +52 -0
  115. package/dist/adapters/cli/tui/types.js +12 -0
  116. package/dist/adapters/sdk/agent.d.ts +20 -0
  117. package/dist/adapters/sdk/agent.js +356 -0
  118. package/dist/adapters/sdk/http.d.ts +43 -0
  119. package/dist/adapters/sdk/http.js +61 -0
  120. package/dist/adapters/sdk/index.d.ts +58 -0
  121. package/dist/adapters/sdk/index.js +209 -0
  122. package/dist/adapters/sdk/settings.d.ts +18 -0
  123. package/dist/adapters/sdk/settings.js +57 -0
  124. package/dist/adapters/sdk/tools.d.ts +7 -0
  125. package/dist/adapters/sdk/tools.js +13 -0
  126. package/dist/adapters/server/auth.d.ts +53 -0
  127. package/dist/adapters/server/auth.js +168 -0
  128. package/dist/adapters/server/index.d.ts +40 -0
  129. package/dist/adapters/server/index.js +255 -0
  130. package/dist/adapters/server/rest-gateway.d.ts +13 -0
  131. package/dist/adapters/server/rest-gateway.js +218 -0
  132. package/dist/adapters/server/rest.d.ts +37 -0
  133. package/dist/adapters/server/rest.js +341 -0
  134. package/dist/adapters/server/server-core.d.ts +55 -0
  135. package/dist/adapters/server/server-core.js +121 -0
  136. package/dist/adapters/server/session-store.d.ts +81 -0
  137. package/dist/adapters/server/session-store.js +272 -0
  138. package/dist/adapters/server/settings-handlers.d.ts +24 -0
  139. package/dist/adapters/server/settings-handlers.js +360 -0
  140. package/dist/adapters/server/standalone.d.ts +19 -0
  141. package/dist/adapters/server/standalone.js +113 -0
  142. package/dist/adapters/server/websocket.d.ts +26 -0
  143. package/dist/adapters/server/websocket.js +68 -0
  144. package/dist/adapters/server/ws-handlers.d.ts +32 -0
  145. package/dist/adapters/server/ws-handlers.js +523 -0
  146. package/dist/adapters/server/ws-types.d.ts +304 -0
  147. package/dist/adapters/server/ws-types.js +7 -0
  148. package/dist/core/agent-loop.d.ts +68 -0
  149. package/dist/core/agent-loop.js +423 -0
  150. package/dist/core/config.d.ts +115 -0
  151. package/dist/core/config.js +189 -0
  152. package/dist/core/errors.d.ts +58 -0
  153. package/dist/core/errors.js +88 -0
  154. package/dist/core/hooks.d.ts +35 -0
  155. package/dist/core/hooks.js +49 -0
  156. package/dist/core/index.d.ts +23 -0
  157. package/dist/core/index.js +29 -0
  158. package/dist/core/message-convert.d.ts +41 -0
  159. package/dist/core/message-convert.js +94 -0
  160. package/dist/core/middleware/auth.d.ts +24 -0
  161. package/dist/core/middleware/auth.js +28 -0
  162. package/dist/core/middleware/logging.d.ts +23 -0
  163. package/dist/core/middleware/logging.js +28 -0
  164. package/dist/core/middleware/rate-limit.d.ts +27 -0
  165. package/dist/core/middleware/rate-limit.js +38 -0
  166. package/dist/core/middleware/semantic-tools.d.ts +10 -0
  167. package/dist/core/middleware/semantic-tools.js +43 -0
  168. package/dist/core/middleware.d.ts +48 -0
  169. package/dist/core/middleware.js +38 -0
  170. package/dist/core/permission.d.ts +25 -0
  171. package/dist/core/permission.js +50 -0
  172. package/dist/core/provider-config.d.ts +129 -0
  173. package/dist/core/provider-config.js +273 -0
  174. package/dist/core/provider-env.d.ts +39 -0
  175. package/dist/core/provider-env.js +142 -0
  176. package/dist/core/provider-resolver.d.ts +12 -0
  177. package/dist/core/provider-resolver.js +12 -0
  178. package/dist/core/session-store.d.ts +75 -0
  179. package/dist/core/session-store.js +245 -0
  180. package/dist/core/settings-manager.d.ts +57 -0
  181. package/dist/core/settings-manager.js +359 -0
  182. package/dist/core/settings-schema.d.ts +38 -0
  183. package/dist/core/settings-schema.js +171 -0
  184. package/dist/core/skill-catalog.d.ts +6 -0
  185. package/dist/core/skill-catalog.js +17 -0
  186. package/dist/core/skill-invoker.d.ts +127 -0
  187. package/dist/core/skill-invoker.js +182 -0
  188. package/dist/core/stream-accumulator.d.ts +21 -0
  189. package/dist/core/stream-accumulator.js +51 -0
  190. package/dist/core/stream-manager.d.ts +58 -0
  191. package/dist/core/stream-manager.js +212 -0
  192. package/dist/core/tool-executor.d.ts +84 -0
  193. package/dist/core/tool-executor.js +256 -0
  194. package/dist/core/types.d.ts +259 -0
  195. package/dist/core/types.js +11 -0
  196. package/dist/gateway/gateway.d.ts +52 -0
  197. package/dist/gateway/gateway.js +537 -0
  198. package/dist/gateway/index.d.ts +21 -0
  199. package/dist/gateway/index.js +31 -0
  200. package/dist/gateway/openapi-importer.d.ts +15 -0
  201. package/dist/gateway/openapi-importer.js +66 -0
  202. package/dist/gateway/semantic-scorer.d.ts +7 -0
  203. package/dist/gateway/semantic-scorer.js +24 -0
  204. package/dist/gateway/settings-adapter.d.ts +49 -0
  205. package/dist/gateway/settings-adapter.js +137 -0
  206. package/dist/gateway/tool-factory.d.ts +9 -0
  207. package/dist/gateway/tool-factory.js +414 -0
  208. package/dist/gateway/types.d.ts +68 -0
  209. package/dist/gateway/types.js +7 -0
  210. package/dist/models-catalog.js +46 -0
  211. package/dist/providers/anthropic.d.ts +22 -0
  212. package/dist/providers/anthropic.js +148 -0
  213. package/dist/providers/factory.d.ts +10 -0
  214. package/dist/providers/factory.js +25 -0
  215. package/dist/providers/openai.d.ts +15 -0
  216. package/dist/providers/openai.js +71 -0
  217. package/dist/providers/types.d.ts +48 -0
  218. package/dist/providers/types.js +1 -0
  219. package/dist/skills/args.d.ts +37 -0
  220. package/dist/skills/args.js +99 -0
  221. package/dist/skills/index.d.ts +11 -0
  222. package/dist/skills/index.js +23 -0
  223. package/dist/skills/loader.d.ts +3 -0
  224. package/dist/skills/loader.js +59 -0
  225. package/dist/skills/parser.d.ts +7 -0
  226. package/dist/skills/parser.js +152 -0
  227. package/dist/skills/registry.d.ts +13 -0
  228. package/dist/skills/registry.js +74 -0
  229. package/dist/skills/resolver.d.ts +19 -0
  230. package/dist/skills/resolver.js +116 -0
  231. package/dist/skills/types.d.ts +74 -0
  232. package/dist/skills/types.js +50 -0
  233. package/dist/tools/browser.d.ts +2 -0
  234. package/dist/tools/browser.js +68 -0
  235. package/dist/tools/core.d.ts +20 -0
  236. package/dist/tools/core.js +244 -0
  237. package/dist/tools/email.d.ts +2 -0
  238. package/dist/tools/email.js +61 -0
  239. package/dist/tools/image.d.ts +2 -0
  240. package/dist/tools/image.js +257 -0
  241. package/dist/tools/index.d.ts +2 -0
  242. package/dist/tools/index.js +88 -0
  243. package/dist/tools/interface.d.ts +22 -0
  244. package/dist/tools/interface.js +1 -0
  245. package/dist/tools/notify.d.ts +2 -0
  246. package/dist/tools/notify.js +100 -0
  247. package/dist/tools/prompt-optimizer.d.ts +2 -0
  248. package/dist/tools/prompt-optimizer.js +65 -0
  249. package/dist/tools/screenshot.d.ts +2 -0
  250. package/dist/tools/screenshot.js +184 -0
  251. package/dist/tools/search.d.ts +2 -0
  252. package/dist/tools/search.js +78 -0
  253. package/dist/tools/todos.d.ts +10 -0
  254. package/dist/tools/todos.js +50 -0
  255. package/package.json +119 -0
  256. package/skills/docker-ops/SKILL.md +329 -0
  257. package/skills/k8s-deploy/SKILL.md +397 -0
  258. package/skills/log-analyzer/SKILL.md +331 -0
  259. package/skills/speckit-analyze/SKILL.md +260 -0
  260. package/skills/speckit-checklist/SKILL.md +374 -0
  261. package/skills/speckit-clarify/SKILL.md +286 -0
  262. package/skills/speckit-constitution/SKILL.md +157 -0
  263. package/skills/speckit-implement/SKILL.md +224 -0
  264. package/skills/speckit-plan/SKILL.md +171 -0
  265. package/skills/speckit-specify/SKILL.md +346 -0
  266. package/skills/speckit-tasks/SKILL.md +215 -0
  267. package/skills/speckit-taskstoissues/SKILL.md +107 -0
@@ -0,0 +1,177 @@
1
+ /**
2
+ * use-agent — agent run state for the TUI.
3
+ *
4
+ * Drives `Agent.chat(input, signal, approveTool, permissionLevel, onStep)`,
5
+ * which (in TUI mode) opts into token streaming — the loop emits `text_delta`
6
+ * steps as tokens arrive. Those accumulate into `streamingText` (rendered live
7
+ * in the message area, since Ink `<Static>` freezes completed entries); on a
8
+ * tool call or turn end the accumulated text is committed to the feed history.
9
+ * ESC/Ctrl+C calls `agent.abort()`.
10
+ *
11
+ * `approveTool` runs inside the detached `runAgentLoop` promise, so it must
12
+ * pause and wait for the user to press y/n in `<PermissionPrompt>`. This hook
13
+ * owns that bridge: it stores the pending resolver in a ref (stable across
14
+ * renders) and the pending prompt's view in state (so the component re-renders).
15
+ */
16
+ import { useCallback, useRef, useState } from 'react';
17
+ export function useAgent({ agent, feed, permissionLevel }) {
18
+ const [isRunning, setIsRunning] = useState(false);
19
+ const [pendingPermission, setPendingPermission] = useState(null);
20
+ const [streamingText, setStreamingText] = useState('');
21
+ const [streamingTool, setStreamingTool] = useState(null);
22
+ const [usage, setUsage] = useState({
23
+ totalPromptTokens: 0,
24
+ totalCompletionTokens: 0,
25
+ totalCost: 0,
26
+ requestCount: 0,
27
+ });
28
+ const [contextTokens, setContextTokens] = useState(0);
29
+ const [latestTodos, setLatestTodos] = useState(null);
30
+ // Refs hold the latest values so the stable callbacks never close over
31
+ // stale state (CLAUDE.md §6: long-lived callbacks read through refs).
32
+ const feedRef = useRef(feed);
33
+ feedRef.current = feed;
34
+ const permissionLevelRef = useRef(permissionLevel);
35
+ permissionLevelRef.current = permissionLevel;
36
+ const resolverRef = useRef(null);
37
+ const streamingTextRef = useRef('');
38
+ const streamingToolRef = useRef(null);
39
+ /** Commit accumulated streaming text to the feed history as an assistant entry. */
40
+ const commitStreaming = useCallback(() => {
41
+ if (streamingTextRef.current) {
42
+ feedRef.current.appendEntry({ kind: 'assistant', content: streamingTextRef.current });
43
+ streamingTextRef.current = '';
44
+ setStreamingText('');
45
+ }
46
+ }, []);
47
+ const submit = useCallback(async (input) => {
48
+ const trimmed = input.trim();
49
+ if (!trimmed)
50
+ return;
51
+ setIsRunning(true);
52
+ streamingTextRef.current = '';
53
+ setStreamingText('');
54
+ streamingToolRef.current = null;
55
+ setStreamingTool(null);
56
+ feedRef.current.appendEntry({ kind: 'user', content: trimmed });
57
+ // Resolve @path file references at the caller, not inside Agent.chat (T022).
58
+ let resolvedInput = trimmed;
59
+ if (trimmed.includes('@')) {
60
+ try {
61
+ const { resolveReferences } = await import('../../../../skills/resolver.js');
62
+ resolvedInput = await resolveReferences(trimmed);
63
+ }
64
+ catch { /* resolver not available — use raw input */ }
65
+ }
66
+ const signal = agent.createAbortSignal();
67
+ const approveTool = async (call) => {
68
+ setPendingPermission({ toolName: call.name, args: call.args });
69
+ const decision = await new Promise((resolve) => {
70
+ resolverRef.current = resolve;
71
+ });
72
+ resolverRef.current = null;
73
+ setPendingPermission(null);
74
+ return decision;
75
+ };
76
+ const onStep = (step) => {
77
+ if (step.type === 'text_delta' && step.content) {
78
+ streamingTextRef.current += step.content;
79
+ setStreamingText(streamingTextRef.current);
80
+ }
81
+ else if (step.type === 'text' && step.content != null) {
82
+ // Non-streaming fallback (defensive; stream mode emits text_delta).
83
+ commitStreaming();
84
+ feedRef.current.appendEntry({ kind: 'assistant', content: step.content });
85
+ }
86
+ else if (step.type === 'tool_progress' && step.content != null) {
87
+ // Live tool output (e.g. streaming shell stdout). Accumulate into a
88
+ // streamingTool block rendered outside <Static> so it repaints per chunk.
89
+ if (streamingToolRef.current) {
90
+ streamingToolRef.current.output += step.content;
91
+ }
92
+ else {
93
+ streamingToolRef.current = {
94
+ name: step.name ?? 'tool',
95
+ args: step.args ?? {},
96
+ output: step.content,
97
+ };
98
+ }
99
+ setStreamingTool(streamingToolRef.current ? { ...streamingToolRef.current } : null);
100
+ }
101
+ else if (step.type === 'tool_call' && step.toolCall) {
102
+ commitStreaming();
103
+ streamingToolRef.current = null;
104
+ setStreamingTool(null);
105
+ const tc = step.toolCall;
106
+ // manage_todos updates the persistent todo panel (not the feed).
107
+ if (tc.name === 'manage_todos') {
108
+ try {
109
+ const parsed = JSON.parse(tc.result);
110
+ if (Array.isArray(parsed))
111
+ setLatestTodos(parsed);
112
+ }
113
+ catch { /* ignore parse error */ }
114
+ return;
115
+ }
116
+ feedRef.current.appendEntry({
117
+ kind: 'tool',
118
+ name: tc.name,
119
+ args: tc.args,
120
+ status: 'ok',
121
+ output: tc.result,
122
+ durationMs: tc.duration,
123
+ metadata: step.metadata,
124
+ });
125
+ }
126
+ };
127
+ try {
128
+ const result = await agent.chat(resolvedInput, signal, approveTool, permissionLevelRef.current, onStep);
129
+ commitStreaming(); // commit the final assistant message if any
130
+ if (result.finishReason === 'error' && result.error) {
131
+ feedRef.current.appendEntry({ kind: 'error', message: result.error });
132
+ }
133
+ if (result.usage) {
134
+ setUsage((u) => ({
135
+ totalPromptTokens: u.totalPromptTokens + (result.usage?.promptTokens ?? 0),
136
+ totalCompletionTokens: u.totalCompletionTokens + (result.usage?.completionTokens ?? 0),
137
+ totalCost: u.totalCost + (result.usage?.cost ?? 0),
138
+ requestCount: u.requestCount + 1,
139
+ }));
140
+ setContextTokens(result.usage.promptTokens ?? 0);
141
+ }
142
+ }
143
+ catch (error) {
144
+ commitStreaming();
145
+ const message = error instanceof Error ? error.message : String(error);
146
+ feedRef.current.appendEntry({ kind: 'error', message });
147
+ }
148
+ finally {
149
+ // Unblock the loop if the agent was aborted mid-approval.
150
+ if (resolverRef.current) {
151
+ resolverRef.current(false);
152
+ resolverRef.current = null;
153
+ }
154
+ setPendingPermission(null);
155
+ setIsRunning(false);
156
+ }
157
+ }, [agent, commitStreaming]);
158
+ const resolvePermission = useCallback((approve) => {
159
+ const resolve = resolverRef.current;
160
+ if (resolve) {
161
+ resolverRef.current = null;
162
+ resolve(approve);
163
+ }
164
+ }, []);
165
+ const abort = useCallback(() => {
166
+ // A pending approval is resolved as a deny so the loop unblocks before abort.
167
+ if (resolverRef.current) {
168
+ resolverRef.current(false);
169
+ resolverRef.current = null;
170
+ setPendingPermission(null);
171
+ }
172
+ agent.abort();
173
+ }, [agent]);
174
+ const resetTodos = useCallback(() => setLatestTodos(null), []);
175
+ const restoreTodos = useCallback((todos) => setLatestTodos(todos), []);
176
+ return { isRunning, pendingPermission, streamingText, streamingTool, usage, contextTokens, latestTodos, submit, resolvePermission, abort, resetTodos, restoreTodos };
177
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * use-feed — owns the feed array.
3
+ *
4
+ * Entries are appended immutably (so completed history can render via Ink's
5
+ * `<Static>` without re-rendering) and updated by id (streaming/permission
6
+ * state). `appendEntry` generates and returns the entry id so callers can
7
+ * patch the same entry later (e.g. resolve a permission prompt).
8
+ */
9
+ import type { FeedEntry, FeedEntryInput } from '../types.js';
10
+ export interface FeedApi {
11
+ entries: FeedEntry[];
12
+ appendEntry: (entry: FeedEntryInput) => string;
13
+ updateEntry: (id: string, patch: Partial<FeedEntry>) => void;
14
+ clear: () => void;
15
+ }
16
+ export declare function useFeed(): FeedApi;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * use-feed — owns the feed array.
3
+ *
4
+ * Entries are appended immutably (so completed history can render via Ink's
5
+ * `<Static>` without re-rendering) and updated by id (streaming/permission
6
+ * state). `appendEntry` generates and returns the entry id so callers can
7
+ * patch the same entry later (e.g. resolve a permission prompt).
8
+ */
9
+ import { useCallback, useState } from 'react';
10
+ import { generateId } from '../../../../core/message-convert.js';
11
+ export function useFeed() {
12
+ const [entries, setEntries] = useState([]);
13
+ const appendEntry = useCallback((entry) => {
14
+ const id = generateId();
15
+ setEntries((prev) => [...prev, { ...entry, id }]);
16
+ return id;
17
+ }, []);
18
+ const updateEntry = useCallback((id, patch) => {
19
+ setEntries((prev) => prev.map((e) => (e.id === id ? { ...e, ...patch } : e)));
20
+ }, []);
21
+ const clear = useCallback(() => {
22
+ setEntries([]);
23
+ }, []);
24
+ return { entries, appendEntry, updateEntry, clear };
25
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Watches the project directory for external file changes (using built-in
3
+ * `fs.watch` with recursive mode — supported on macOS/Windows). Fires only
4
+ * when the agent is idle (not running) so the agent's own writes don't
5
+ * trigger false notifications. Debounced (fs.watch fires multiple events).
6
+ */
7
+ export declare function useFileWatcher(idle: boolean): {
8
+ changedFile: string | null;
9
+ clear: () => void;
10
+ };
@@ -0,0 +1,43 @@
1
+ import { useEffect, useState, useCallback } from 'react';
2
+ import * as fs from 'fs';
3
+ const IGNORED_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'out', 'coverage', '.cache']);
4
+ /**
5
+ * Watches the project directory for external file changes (using built-in
6
+ * `fs.watch` with recursive mode — supported on macOS/Windows). Fires only
7
+ * when the agent is idle (not running) so the agent's own writes don't
8
+ * trigger false notifications. Debounced (fs.watch fires multiple events).
9
+ */
10
+ export function useFileWatcher(idle) {
11
+ const [changedFile, setChangedFile] = useState(null);
12
+ useEffect(() => {
13
+ if (!idle)
14
+ return; // only watch when idle (agent's writes suppressed)
15
+ let timer = null;
16
+ let watcher = null;
17
+ try {
18
+ watcher = fs.watch(process.cwd(), { recursive: true }, (_eventType, filename) => {
19
+ if (!filename)
20
+ return;
21
+ const parts = filename.split(/[/\\]/);
22
+ if (parts.some((p) => IGNORED_DIRS.has(p) || p.startsWith('.')))
23
+ return;
24
+ if (timer)
25
+ clearTimeout(timer);
26
+ timer = setTimeout(() => {
27
+ setChangedFile(filename);
28
+ timer = null;
29
+ }, 600);
30
+ });
31
+ }
32
+ catch {
33
+ // recursive watch not supported (Linux) — no-op.
34
+ }
35
+ return () => {
36
+ watcher?.close();
37
+ if (timer)
38
+ clearTimeout(timer);
39
+ };
40
+ }, [idle]);
41
+ const clear = useCallback(() => setChangedFile(null), []);
42
+ return { changedFile, clear };
43
+ }
@@ -0,0 +1,16 @@
1
+ export interface KeybindingHandlers {
2
+ onAbort: () => void;
3
+ onExit: () => void;
4
+ onExpandToggle: () => void;
5
+ onPalette: () => void;
6
+ onClear: () => void;
7
+ }
8
+ /**
9
+ * Global keybindings. Disabled while a modal overlay is open (`enabled: false`)
10
+ * so the overlay owns input. Ctrl+C aborts mid-run or exits when idle.
11
+ * (Help is `/?`, not bare `?` — bare `?` would fire mid-question.)
12
+ */
13
+ export declare function useKeybindings(handlers: KeybindingHandlers, opts: {
14
+ enabled: boolean;
15
+ isRunning: boolean;
16
+ }): void;
@@ -0,0 +1,25 @@
1
+ import { useInput } from 'ink';
2
+ /**
3
+ * Global keybindings. Disabled while a modal overlay is open (`enabled: false`)
4
+ * so the overlay owns input. Ctrl+C aborts mid-run or exits when idle.
5
+ * (Help is `/?`, not bare `?` — bare `?` would fire mid-question.)
6
+ */
7
+ export function useKeybindings(handlers, opts) {
8
+ useInput((input, key) => {
9
+ if (!opts.enabled)
10
+ return;
11
+ if (key.ctrl) {
12
+ if (input === 'o' || input === '\x0f')
13
+ handlers.onExpandToggle();
14
+ else if (input === 'p' || input === '\x10')
15
+ handlers.onPalette();
16
+ else if (input === 'l' || input === '\x0c')
17
+ handlers.onClear();
18
+ else if (input === 'c' || input === '\x03')
19
+ (opts.isRunning ? handlers.onAbort() : handlers.onExit());
20
+ return;
21
+ }
22
+ if (key.escape)
23
+ handlers.onAbort();
24
+ });
25
+ }
@@ -0,0 +1,8 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type Theme } from '../theme.js';
3
+ /** Provides the theme to all descendants. Wrap the TUI root once. */
4
+ export declare function ThemeProvider({ children }: {
5
+ children: ReactNode;
6
+ }): import("react").JSX.Element;
7
+ /** Access the active theme. Single source of color tokens in components. */
8
+ export declare function useTheme(): Theme;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext } from 'react';
3
+ import { theme } from '../theme.js';
4
+ const ThemeContext = createContext(theme);
5
+ /** Provides the theme to all descendants. Wrap the TUI root once. */
6
+ export function ThemeProvider({ children }) {
7
+ return _jsx(ThemeContext.Provider, { value: theme, children: children });
8
+ }
9
+ /** Access the active theme. Single source of color tokens in components. */
10
+ export function useTheme() {
11
+ return useContext(ThemeContext);
12
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * TUI public entry — lazy-loaded by `src/adapters/cli/index.ts` only when
3
+ * `resolveLaunchMode(options) === 'interactive'`. Headless / piped / --docker
4
+ * modes never import this module, so React/Ink stay out of memory there.
5
+ *
6
+ * The `.tsx` extension is required (the file contains JSX); the lazy import
7
+ * specifier `./tui/index.js` resolves to this source under `tsx` dev mode and
8
+ * to the compiled `dist/adapters/cli/tui/index.js` under `tsc`.
9
+ */
10
+ export interface StartTuiArgs {
11
+ queryParts: string[];
12
+ options: any;
13
+ }
14
+ /**
15
+ * Run the shared session setup (same bootstrap as the readline REPL), build the
16
+ * shared command registry, then render the full-screen Ink TUI. `exitOnCtrlC`
17
+ * is false so the app owns Ctrl+C (abort mid-run, exit when idle) per FR-006.
18
+ */
19
+ export declare function startTui({ queryParts, options }: StartTuiArgs): Promise<void>;
@@ -0,0 +1,206 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * TUI public entry — lazy-loaded by `src/adapters/cli/index.ts` only when
4
+ * `resolveLaunchMode(options) === 'interactive'`. Headless / piped / --docker
5
+ * modes never import this module, so React/Ink stay out of memory there.
6
+ *
7
+ * The `.tsx` extension is required (the file contains JSX); the lazy import
8
+ * specifier `./tui/index.js` resolves to this source under `tsx` dev mode and
9
+ * to the compiled `dist/adapters/cli/tui/index.js` under `tsc`.
10
+ */
11
+ import { render } from 'ink';
12
+ import { TuiApp } from './app.js';
13
+ import { formatJson, formatTranscript } from './session-export.js';
14
+ import * as fs from 'node:fs';
15
+ import * as path from 'node:path';
16
+ import { bootstrapCliSession } from '../bootstrap.js';
17
+ import { buildCommandRegistry } from '../commands/build-registry.js';
18
+ import { warmInkReset, resetInkStatic } from './ink-reset.js';
19
+ import { ThemeProvider } from './hooks/use-theme.js';
20
+ import { MODEL_CATALOG } from '../../../models-catalog.js';
21
+ import { resolveProviderConfigFromApp } from '../../../core/provider-resolver.js';
22
+ import { createProvider } from '../../../providers/factory.js';
23
+ import { SettingsManager } from '../../../core/settings-manager.js';
24
+ import { SETTINGS_MAP, SETTINGS_SCHEMA } from '../../../core/settings-schema.js';
25
+ import { loadMergedConfig, loadJsonConfig, getConfigPaths, applyEnvOverrides } from '../config-loader.js';
26
+ /**
27
+ * Run the shared session setup (same bootstrap as the readline REPL), build the
28
+ * shared command registry, then render the full-screen Ink TUI. `exitOnCtrlC`
29
+ * is false so the app owns Ctrl+C (abort mid-run, exit when idle) per FR-006.
30
+ */
31
+ export async function startTui({ queryParts, options }) {
32
+ const initialQuery = queryParts.join(' ').trim();
33
+ const ctx = await bootstrapCliSession(options);
34
+ const { agent, fullConfig, activeProviderType, gatewayInstance, permissionLevel, persistence } = ctx;
35
+ // Same registry the readline REPL uses — one owner of the command set.
36
+ const registry = buildCommandRegistry(agent, fullConfig, activeProviderType, gatewayInstance);
37
+ // Autocomplete sources: built-in commands + loaded skills.
38
+ const commands = registry.getAll()
39
+ .filter((e) => !e.hidden)
40
+ .map((e) => ({ name: e.name, description: e.description }));
41
+ const skills = (agent.getSkillRegistry()?.getAll() ?? [])
42
+ .map((s) => ({ name: s.name, description: s.description }));
43
+ // Model-selector options: catalog models for each CONFIGURED provider.
44
+ const modelOptions = [];
45
+ for (const pt of ['openai', 'anthropic', 'glm', 'openai-compatible']) {
46
+ if (!resolveProviderConfigFromApp(fullConfig, pt))
47
+ continue;
48
+ for (const m of MODEL_CATALOG[pt]) {
49
+ modelOptions.push({ providerType: pt, modelId: m.id, modelName: m.name });
50
+ }
51
+ }
52
+ const onSwitchModel = async (providerType, modelId) => {
53
+ const pc = resolveProviderConfigFromApp(fullConfig, providerType);
54
+ if (!pc)
55
+ return;
56
+ pc.model = modelId;
57
+ const provider = await createProvider(pc);
58
+ agent.switchProvider(provider, modelId);
59
+ };
60
+ // Fresh settings list each time the overlay opens (so edits via /settings set
61
+ // are reflected without a restart).
62
+ const getSettingsList = () => {
63
+ const paths = getConfigPaths();
64
+ const sm = new SettingsManager({
65
+ config: applyEnvOverrides(loadMergedConfig()),
66
+ projectConfigPath: paths.local,
67
+ globalConfigPath: paths.global,
68
+ projectConfig: loadJsonConfig(paths.local),
69
+ globalConfig: loadJsonConfig(paths.global),
70
+ });
71
+ return sm.list().map((s) => {
72
+ const mapEntry = SETTINGS_MAP.get(s.dotKey);
73
+ const schemaEntry = SETTINGS_SCHEMA.get(s.dotKey);
74
+ return {
75
+ dotKey: s.dotKey,
76
+ value: s.masked ? '******' : String(s.value),
77
+ category: s.category,
78
+ label: mapEntry?.label ?? s.dotKey,
79
+ type: schemaEntry?.type ?? 'string',
80
+ secret: schemaEntry?.secret ?? false,
81
+ enumValues: schemaEntry?.enumValues,
82
+ restartRequired: schemaEntry?.restartRequired ?? false,
83
+ };
84
+ });
85
+ };
86
+ const onSetSetting = async (dotKey, value) => {
87
+ const paths = getConfigPaths();
88
+ const sm = new SettingsManager({
89
+ config: applyEnvOverrides(loadMergedConfig()),
90
+ projectConfigPath: paths.local,
91
+ globalConfigPath: paths.global,
92
+ projectConfig: loadJsonConfig(paths.local),
93
+ globalConfig: loadJsonConfig(paths.global),
94
+ });
95
+ await sm.set(dotKey, value);
96
+ };
97
+ // ── Sessions ────────────────────────────────────────────────────────────
98
+ // list() returns bare ids; load() each to get metadata for the selector.
99
+ // N+1 I/O is fine for O(10s) of local sessions. Derive a preview from the
100
+ // first user message (SessionData has no title field). Forward-compatible:
101
+ // swap this closure for registry.sessionsForUser() once 002 lands (see
102
+ // specs/002-channels-integration migration note).
103
+ const PREVIEW_LEN = 80;
104
+ const listSessions = async () => {
105
+ const ids = await persistence.list();
106
+ const loaded = await Promise.all(ids.map((id) => persistence.load(id)));
107
+ return loaded
108
+ .filter((s) => s != null)
109
+ .map((s) => {
110
+ const firstUser = s.messages.find((m) => m.role === 'user');
111
+ const preview = (firstUser?.content ?? s.id).split('\n')[0].trim();
112
+ return {
113
+ id: s.id,
114
+ preview: preview.length > PREVIEW_LEN ? preview.slice(0, PREVIEW_LEN - 1) + '…' : preview,
115
+ updatedAt: s.updatedAt,
116
+ userMessageCount: s.messages.filter((m) => m.role === 'user').length,
117
+ toolCallCount: s.messages.filter((m) => m.role === 'tool').length,
118
+ assistantMessageCount: s.messages.filter((m) => m.role === 'assistant').length,
119
+ title: s.metadata?.title,
120
+ provider: s.provider,
121
+ model: s.model,
122
+ };
123
+ })
124
+ .sort((a, b) => b.updatedAt - a.updatedAt);
125
+ };
126
+ // Load the session into the agent and return a short summary for the info
127
+ // notice. The caller (TuiApp) rebuilds the feed from agent.getMessages().
128
+ const onSwitchSession = async (sessionId) => {
129
+ const ok = await agent.loadSession(sessionId);
130
+ if (!ok)
131
+ return null;
132
+ const msgs = agent.getMessages();
133
+ const firstUser = msgs.find((m) => m.role === 'user');
134
+ const preview = (firstUser?.content ?? sessionId).split('\n')[0].trim();
135
+ return {
136
+ preview,
137
+ userMessageCount: msgs.filter((m) => m.role === 'user').length,
138
+ toolCallCount: msgs.filter((m) => m.role === 'tool').length,
139
+ };
140
+ };
141
+ const onDeleteSession = async (sessionId) => {
142
+ await persistence.delete(sessionId);
143
+ };
144
+ // Export: write full SessionData as JSON to ./<short-id>.json. Returns the
145
+ // written path so the app can show a confirmation notice.
146
+ const onExportSession = async (sessionId) => {
147
+ const data = await persistence.load(sessionId);
148
+ if (!data)
149
+ return null;
150
+ const outPath = path.join(process.cwd(), `${sessionId.slice(0, 8)}.json`);
151
+ await fs.promises.writeFile(outPath, formatJson(data), 'utf-8');
152
+ return outPath;
153
+ };
154
+ // Transcript: write a human-readable Markdown render to ./<short-id>.md.
155
+ const onTranscriptSession = async (sessionId) => {
156
+ const data = await persistence.load(sessionId);
157
+ if (!data)
158
+ return null;
159
+ const outPath = path.join(process.cwd(), `${sessionId.slice(0, 8)}.md`);
160
+ await fs.promises.writeFile(outPath, formatTranscript(data), 'utf-8');
161
+ return outPath;
162
+ };
163
+ // Rename: set metadata.title via a full round-trip (load → set title → save).
164
+ // The backend's metadata merge (`data.metadata ?? existing.metadata`) means
165
+ // regular chat saves (which pass metadata: undefined) preserve the title.
166
+ const onRenameSession = async (sessionId, title) => {
167
+ const data = await persistence.load(sessionId);
168
+ if (!data)
169
+ return false;
170
+ await persistence.save(sessionId, {
171
+ ...data,
172
+ metadata: { ...data.metadata, title },
173
+ });
174
+ return true;
175
+ };
176
+ // Bridge the registry to the TUI: defer interactive (stdin/stdout-owning)
177
+ // commands; otherwise dispatch and surface the returned output.
178
+ const dispatchCommand = async (input) => {
179
+ const entry = registry.resolveCommand(input);
180
+ if (entry?.interactive)
181
+ return { status: 'handled', deferred: true };
182
+ const { status, output } = await registry.dispatch(input, { agent, args: '', config: fullConfig }, agent.getSkillRegistry());
183
+ const isExit = status === 'exit';
184
+ return { status: isExit ? 'handled' : status, output, exit: isExit };
185
+ };
186
+ // Clear any bootstrap status output (Loaded config / Gateway initialized) so
187
+ // the TUI launches on a clean screen.
188
+ process.stdout.write('\x1B[2J\x1B[1;1H');
189
+ // Pre-load Ink's internal instances store (absolute-path import; see
190
+ // ink-reset.ts) so resize/expand resets are synchronous.
191
+ await warmInkReset();
192
+ let instance;
193
+ const onExit = () => {
194
+ instance.unmount();
195
+ process.exit(0);
196
+ };
197
+ // Reset Ink's accumulated Static output + clear the screen before a `<Static>`
198
+ // remount (resize / expand-toggle), so history repaints cleanly without
199
+ // phantom duplicates. (Command Code's fullStaticOutput reset pattern.)
200
+ const resetView = () => {
201
+ resetInkStatic(process.stdout);
202
+ instance.clear();
203
+ process.stdout.write('\x1B[2J\x1B[3J\x1B[H');
204
+ };
205
+ instance = render(_jsx(ThemeProvider, { children: _jsx(TuiApp, { agent: agent, permissionLevel: permissionLevel, initialQuery: initialQuery, onExit: onExit, dispatchCommand: dispatchCommand, commands: commands, skills: skills, resetView: resetView, providerType: activeProviderType, gatewayOn: !!gatewayInstance, skillCount: skills.length, mcpCount: gatewayInstance?.getTargets ? Object.keys(gatewayInstance.getTargets()).length : 0, modelOptions: modelOptions, onSwitchModel: onSwitchModel, getSettingsList: getSettingsList, onSetSetting: onSetSetting, listSessions: listSessions, onSwitchSession: onSwitchSession, onDeleteSession: onDeleteSession, onExportSession: onExportSession, onTranscriptSession: onTranscriptSession, onRenameSession: onRenameSession, getSessionId: () => agent.getSessionId() }) }), { exitOnCtrlC: false });
206
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Ink internals reset — the Command Code pattern, adapted for stock Ink 6.6.0
3
+ * without a bundler.
4
+ *
5
+ * Why: `<Static>` renders each item once and freezes it, so resize-reflow and
6
+ * tool-block expand/collapse (which need to re-render history) don't work.
7
+ * Remounting `<Static>` via a `key` bump re-paints everything — BUT Ink
8
+ * accumulates the re-emitted items in its internal `fullStaticOutput`, causing
9
+ * duplicate "phantom" lines. Resetting `fullStaticOutput` + `lastOutput` before
10
+ * the remount fixes that.
11
+ *
12
+ * Stock Ink doesn't expose the instance. Its `package.json` `exports` field
13
+ * blocks `ink/build/instances.js` as a subpath, BUT importing the file by its
14
+ * resolved absolute path (derived from the exported main entry) bypasses
15
+ * package-exports enforcement — no bundler required.
16
+ *
17
+ * This is internals-poking, so it's shape-guarded: if a future Ink changes the
18
+ * internal field names or the instances store, `resetInkStatic` returns false
19
+ * and callers fall back to the frozen-on-resize behavior (no crash). Mirror of
20
+ * Command Code's `registerInkControl` + `warnPatchSkipped`.
21
+ */
22
+ /** Pre-load the internal instances store (async; call once at TUI start). */
23
+ export declare function warmInkReset(): Promise<void>;
24
+ /**
25
+ * Reset Ink's accumulated Static output + last-frame tracking so a `<Static>`
26
+ * remount repaints cleanly. Returns false (no-op) if the internals are
27
+ * unavailable or shaped differently than expected.
28
+ */
29
+ export declare function resetInkStatic(stdout: object): boolean;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Ink internals reset — the Command Code pattern, adapted for stock Ink 6.6.0
3
+ * without a bundler.
4
+ *
5
+ * Why: `<Static>` renders each item once and freezes it, so resize-reflow and
6
+ * tool-block expand/collapse (which need to re-render history) don't work.
7
+ * Remounting `<Static>` via a `key` bump re-paints everything — BUT Ink
8
+ * accumulates the re-emitted items in its internal `fullStaticOutput`, causing
9
+ * duplicate "phantom" lines. Resetting `fullStaticOutput` + `lastOutput` before
10
+ * the remount fixes that.
11
+ *
12
+ * Stock Ink doesn't expose the instance. Its `package.json` `exports` field
13
+ * blocks `ink/build/instances.js` as a subpath, BUT importing the file by its
14
+ * resolved absolute path (derived from the exported main entry) bypasses
15
+ * package-exports enforcement — no bundler required.
16
+ *
17
+ * This is internals-poking, so it's shape-guarded: if a future Ink changes the
18
+ * internal field names or the instances store, `resetInkStatic` returns false
19
+ * and callers fall back to the frozen-on-resize behavior (no crash). Mirror of
20
+ * Command Code's `registerInkControl` + `warnPatchSkipped`.
21
+ */
22
+ import { createRequire } from 'node:module';
23
+ import * as path from 'node:path';
24
+ const nodeRequire = createRequire(import.meta.url);
25
+ const inkBuildDir = path.dirname(nodeRequire.resolve('ink'));
26
+ let inkInstances;
27
+ /** Pre-load the internal instances store (async; call once at TUI start). */
28
+ export async function warmInkReset() {
29
+ if (inkInstances !== undefined)
30
+ return;
31
+ try {
32
+ // Absolute-path import sidesteps Ink's `exports` restriction.
33
+ const mod = await import(path.join(inkBuildDir, 'instances.js'));
34
+ inkInstances = mod.default instanceof WeakMap ? mod.default : null;
35
+ }
36
+ catch {
37
+ inkInstances = null;
38
+ }
39
+ }
40
+ /**
41
+ * Reset Ink's accumulated Static output + last-frame tracking so a `<Static>`
42
+ * remount repaints cleanly. Returns false (no-op) if the internals are
43
+ * unavailable or shaped differently than expected.
44
+ */
45
+ export function resetInkStatic(stdout) {
46
+ if (!inkInstances)
47
+ return false;
48
+ const ink = inkInstances.get(stdout);
49
+ if (!ink)
50
+ return false;
51
+ if (typeof ink.fullStaticOutput !== 'string' || typeof ink.lastOutput !== 'string') {
52
+ return false;
53
+ }
54
+ ink.fullStaticOutput = '';
55
+ ink.lastOutput = '';
56
+ return true;
57
+ }