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,15 @@
1
+ /**
2
+ * Shared layout constants for the TUI.
3
+ *
4
+ * `HORIZONTAL_PADDING` insets every rendered line from the terminal edges.
5
+ * It is applied both to the root `<Box>` (live area: prompt, status,
6
+ * permission) and to each `<Static>` item (message history) — Ink's `<Static>`
7
+ * renders at full terminal width and ignores parent padding, so history items
8
+ * must pad themselves.
9
+ *
10
+ * Why pad at all: a line that fills the terminal's final column triggers an
11
+ * auto-wrap (a phantom blank row below). Keeping every line ≤ `columns - 2`
12
+ * avoids that and gives the TUI a gutter on both sides; Ink reflows within the
13
+ * reduced width on resize.
14
+ */
15
+ export declare const HORIZONTAL_PADDING = 1;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Shared layout constants for the TUI.
3
+ *
4
+ * `HORIZONTAL_PADDING` insets every rendered line from the terminal edges.
5
+ * It is applied both to the root `<Box>` (live area: prompt, status,
6
+ * permission) and to each `<Static>` item (message history) — Ink's `<Static>`
7
+ * renders at full terminal width and ignores parent padding, so history items
8
+ * must pad themselves.
9
+ *
10
+ * Why pad at all: a line that fills the terminal's final column triggers an
11
+ * auto-wrap (a phantom blank row below). Keeping every line ≤ `columns - 2`
12
+ * avoids that and gives the TUI a gutter on both sides; Ink reflows within the
13
+ * reduced width on resize.
14
+ */
15
+ export const HORIZONTAL_PADDING = 1;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Tokyo Night rainbow gradient for the Zoe Agent logo. Pure, deterministic, no
3
+ * dependency. Each cell at (row, col) within a (rows × cols) grid is projected
4
+ * onto the 45° axis: `t = (col + row) / ((cols-1) + (rows-1))` — sweeps
5
+ * bottom-left → top-right; for a single row it reduces to a horizontal sweep.
6
+ *
7
+ * Interpolation is RGB between ADJACENT palette stops (red→orange→yellow→green
8
+ * →cyan→blue→purple). Adjacent rainbow hues lerp cleanly (no muddy mid-tones),
9
+ * and since the stops ARE the Tokyo Night accents, the result stays on-palette.
10
+ */
11
+ export declare function rainbowCellColor(row: number, col: number, rows: number, cols: number, stops: readonly string[]): string;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Tokyo Night rainbow gradient for the Zoe Agent logo. Pure, deterministic, no
3
+ * dependency. Each cell at (row, col) within a (rows × cols) grid is projected
4
+ * onto the 45° axis: `t = (col + row) / ((cols-1) + (rows-1))` — sweeps
5
+ * bottom-left → top-right; for a single row it reduces to a horizontal sweep.
6
+ *
7
+ * Interpolation is RGB between ADJACENT palette stops (red→orange→yellow→green
8
+ * →cyan→blue→purple). Adjacent rainbow hues lerp cleanly (no muddy mid-tones),
9
+ * and since the stops ARE the Tokyo Night accents, the result stays on-palette.
10
+ */
11
+ export function rainbowCellColor(row, col, rows, cols, stops) {
12
+ const maxCol = Math.max(0, cols - 1);
13
+ const maxRow = Math.max(0, rows - 1);
14
+ const span = maxCol + maxRow;
15
+ const t = span <= 0 ? 0 : Math.max(0, Math.min(1, (col + row) / span));
16
+ const n = Math.max(1, stops.length - 1);
17
+ const seg = t * n;
18
+ const i = Math.min(Math.floor(seg), stops.length - 2);
19
+ const frac = seg - i;
20
+ const a = hexToRgb(stops[i]);
21
+ const b = hexToRgb(stops[i + 1]);
22
+ return rgbToHex(a[0] + (b[0] - a[0]) * frac, a[1] + (b[1] - a[1]) * frac, a[2] + (b[2] - a[2]) * frac);
23
+ }
24
+ function hexToRgb(hex) {
25
+ const h = hex.replace('#', '');
26
+ return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
27
+ }
28
+ function rgbToHex(r, g, b) {
29
+ const c = (n) => Math.round(Math.max(0, Math.min(255, n))).toString(16).padStart(2, '0');
30
+ return `#${c(r)}${c(g)}${c(b)}`;
31
+ }
@@ -0,0 +1,4 @@
1
+ /** Keybinding reference overlay. Esc (or ?) to close. */
2
+ export declare function HelpDialog({ onClose }: {
3
+ onClose: () => void;
4
+ }): import("react").JSX.Element;
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { useTheme } from '../hooks/use-theme.js';
4
+ const KEYS = [
5
+ ['Ctrl+P', 'Command palette'],
6
+ ['Ctrl+L', 'Clear conversation'],
7
+ ['Ctrl+O', 'Expand/collapse tool output'],
8
+ ['Ctrl+C', 'Abort run / exit (idle)'],
9
+ ['Esc', 'Abort current run'],
10
+ ['Enter', 'Send message (accepts autocomplete)'],
11
+ ['Shift+Enter', 'New line (also Alt+Enter / Ctrl+J)'],
12
+ ['↑ / ↓', 'Move lines (history at top/bottom edge)'],
13
+ ['Tab', 'Accept autocomplete'],
14
+ ['↑/↓', 'Navigate autocomplete'],
15
+ ['/ + Enter', 'Run slash command'],
16
+ ['@path', 'Insert file reference'],
17
+ ];
18
+ /** Keybinding reference overlay. Esc (or ?) to close. */
19
+ export function HelpDialog({ onClose }) {
20
+ const theme = useTheme();
21
+ useInput((input, key) => {
22
+ if (key.escape || input === '?')
23
+ onClose();
24
+ });
25
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.cyan, paddingLeft: 1, paddingRight: 1, children: [_jsx(Text, { color: theme.cyan, bold: true, children: "Keyboard Shortcuts" }), KEYS.map(([k, desc]) => (_jsxs(Box, { children: [_jsx(Text, { color: theme.green, children: k.padEnd(16) }), _jsx(Text, { color: theme.fgDim, children: desc })] }, k))), _jsx(Text, { color: theme.fgDim, children: "Esc to close" })] }));
26
+ }
@@ -0,0 +1,14 @@
1
+ export interface ModelOption {
2
+ providerType: string;
3
+ modelId: string;
4
+ modelName: string;
5
+ }
6
+ interface ModelSelectorProps {
7
+ options: ModelOption[];
8
+ currentModel: string;
9
+ onSwitch: (providerType: string, modelId: string) => void;
10
+ onClose: () => void;
11
+ }
12
+ /** Ctrl+M provider/model picker. ↑/↓ navigate, Enter switches, Esc closes. */
13
+ export declare function ModelSelector({ options, currentModel, onSwitch, onClose }: ModelSelectorProps): import("react").JSX.Element;
14
+ export {};
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import { useTheme } from '../hooks/use-theme.js';
5
+ const MAX_VISIBLE = 10;
6
+ /** Ctrl+M provider/model picker. ↑/↓ navigate, Enter switches, Esc closes. */
7
+ export function ModelSelector({ options, currentModel, onSwitch, onClose }) {
8
+ const theme = useTheme();
9
+ const [selected, setSelected] = useState(() => {
10
+ const idx = options.findIndex((o) => o.modelId === currentModel);
11
+ return idx >= 0 ? idx : 0;
12
+ });
13
+ useInput((_input, key) => {
14
+ if (key.escape) {
15
+ onClose();
16
+ return;
17
+ }
18
+ if (key.return) {
19
+ const o = options[Math.min(selected, options.length - 1)];
20
+ if (o)
21
+ onSwitch(o.providerType, o.modelId);
22
+ return;
23
+ }
24
+ if (key.upArrow)
25
+ setSelected((i) => Math.max(0, i - 1));
26
+ else if (key.downArrow)
27
+ setSelected((i) => Math.min(options.length - 1, i + 1));
28
+ });
29
+ if (options.length === 0) {
30
+ return (_jsx(Box, { borderStyle: "round", borderColor: theme.purple, paddingLeft: 1, paddingRight: 1, children: _jsx(Text, { color: theme.fgDim, children: "No configured providers with models. Set API keys via /setup." }) }));
31
+ }
32
+ const half = Math.floor(MAX_VISIBLE / 2);
33
+ const start = Math.max(0, Math.min(selected - half, options.length - MAX_VISIBLE));
34
+ const visible = options.slice(start, start + MAX_VISIBLE);
35
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.purple, paddingLeft: 1, paddingRight: 1, children: [_jsx(Text, { color: theme.purple, bold: true, children: "Model Selector" }), _jsx(Text, { color: theme.fgDim, children: "\u2191/\u2193 navigate \u00B7 Enter switches model + provider \u00B7 Esc close" }), visible.map((o, i) => {
36
+ const absIdx = start + i;
37
+ const sel = absIdx === selected;
38
+ const isCurrent = o.modelId === currentModel;
39
+ const prevPt = i > 0 ? visible[i - 1].providerType : (start > 0 ? options[start - 1].providerType : null);
40
+ const showHeader = o.providerType !== prevPt;
41
+ return (_jsxs(Box, { flexDirection: "column", children: [showHeader ? _jsxs(Text, { color: theme.purple, bold: true, children: [" ", o.providerType] }) : null, _jsxs(Text, { backgroundColor: sel ? theme.blue : undefined, color: sel ? theme.bg : isCurrent ? theme.green : theme.fg, children: [sel ? '▶ ' : ' ', isCurrent ? '✓ ' : ' ', o.modelName] })] }, `${o.providerType}/${o.modelId}`));
42
+ }), options.length > MAX_VISIBLE ? (_jsxs(Text, { color: theme.fgDim, children: [" ", options.length - MAX_VISIBLE, " more"] })) : null] }));
43
+ }
@@ -0,0 +1,35 @@
1
+ /** A saved session, projected for the selector. */
2
+ export interface SessionListItem {
3
+ id: string;
4
+ /** Searchable display text — first user message, truncated. */
5
+ preview: string;
6
+ /** Epoch ms of the last update. */
7
+ updatedAt: number;
8
+ /** role:"user" count — a proxy for "turns". */
9
+ userMessageCount: number;
10
+ /** role:"tool" count — how many tool calls executed. */
11
+ toolCallCount: number;
12
+ /** role:"assistant" count — model responses (≈ turns). */
13
+ assistantMessageCount: number;
14
+ /** Optional user-set title (metadata.title); takes precedence over preview. */
15
+ title?: string;
16
+ provider?: string;
17
+ model?: string;
18
+ }
19
+ interface SessionSelectorProps {
20
+ sessions: SessionListItem[];
21
+ currentSessionId: string;
22
+ onSelect: (id: string) => void;
23
+ onDelete: (id: string) => void;
24
+ onExport: (id: string) => void;
25
+ onTranscript: (id: string) => void;
26
+ onRename: (id: string, title: string) => Promise<boolean>;
27
+ onClose: () => void;
28
+ }
29
+ /**
30
+ * `/sessions` overlay: fuzzy search to filter, ↑/↓ navigate, Enter resumes,
31
+ * `d` deletes, `e` exports JSON, `t` writes transcript, `r` renames,
32
+ * Esc closes. Rename mode shows a TextInput inline; Enter commits, Esc cancels.
33
+ */
34
+ export declare function SessionSelector({ sessions, currentSessionId, onSelect, onDelete, onExport, onTranscript, onRename, onClose }: SessionSelectorProps): import("react").JSX.Element;
35
+ export {};
@@ -0,0 +1,162 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import { useTheme } from '../hooks/use-theme.js';
5
+ import { TextInput } from '../components/text-input.js';
6
+ const MAX_VISIBLE = 10;
7
+ const PREVIEW_LEN = 60;
8
+ /**
9
+ * `/sessions` overlay: fuzzy search to filter, ↑/↓ navigate, Enter resumes,
10
+ * `d` deletes, `e` exports JSON, `t` writes transcript, `r` renames,
11
+ * Esc closes. Rename mode shows a TextInput inline; Enter commits, Esc cancels.
12
+ */
13
+ export function SessionSelector({ sessions, currentSessionId, onSelect, onDelete, onExport, onTranscript, onRename, onClose }) {
14
+ const theme = useTheme();
15
+ const [query, setQuery] = useState('');
16
+ const [selected, setSelected] = useState(() => {
17
+ const idx = sessions.findIndex((s) => s.id === currentSessionId);
18
+ return idx >= 0 ? idx : 0;
19
+ });
20
+ const [hint, setHint] = useState(null);
21
+ const [renameMode, setRenameMode] = useState(false);
22
+ const [renameValue, setRenameValue] = useState('');
23
+ // Subsequence match on preview (case-insensitive). Keeps the component
24
+ // self-contained — sessions carry their own id, no projection needed.
25
+ const matches = query
26
+ ? sessions.filter((s) => isSubsequence((s.title ?? s.preview).toLowerCase(), query.toLowerCase()))
27
+ : sessions;
28
+ // Clamp the cursor when the list shrinks (delete, filter) so it never points
29
+ // past the last row — otherwise the highlight silently disappears.
30
+ useEffect(() => {
31
+ setSelected((i) => Math.min(i, Math.max(0, matches.length - 1)));
32
+ }, [matches.length]);
33
+ const currentMatch = () => matches[Math.min(selected, matches.length - 1)] ?? matches[0];
34
+ // Rename mode owns input entirely (TextInput component).
35
+ if (renameMode) {
36
+ const submitRename = async () => {
37
+ const title = renameValue.trim();
38
+ const m = currentMatch();
39
+ if (m && title) {
40
+ const ok = await onRename(m.id, title);
41
+ setHint(ok ? `Renamed to "${title}"` : `Rename failed.`);
42
+ }
43
+ setRenameMode(false);
44
+ setRenameValue('');
45
+ };
46
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.purple, paddingLeft: 1, paddingRight: 1, children: [_jsx(Text, { color: theme.purple, bold: true, children: "Rename session" }), _jsx(TextInput, { value: renameValue, onChange: setRenameValue, onSubmit: () => { void submitRename(); }, placeholder: "Enter new title\u2026" }), _jsx(Text, { color: theme.fgDim, children: "Enter to confirm \u00B7 Esc to cancel" })] }));
47
+ }
48
+ useInput((input, key) => {
49
+ if (hint)
50
+ setHint(null);
51
+ if (key.escape) {
52
+ onClose();
53
+ return;
54
+ }
55
+ if (key.return) {
56
+ const m = currentMatch();
57
+ if (m)
58
+ onSelect(m.id);
59
+ return;
60
+ }
61
+ if (key.upArrow) {
62
+ setSelected((i) => Math.max(0, i - 1));
63
+ return;
64
+ }
65
+ if (key.downArrow) {
66
+ setSelected((i) => Math.min(matches.length - 1, i + 1));
67
+ return;
68
+ }
69
+ // Action keys (d/e/t/r) only fire when NOT searching — so typing those
70
+ // letters into the query box works normally. When the query is empty,
71
+ // they operate on the selected row.
72
+ if (!query && input && !key.ctrl && !key.meta) {
73
+ if (input === 'd') {
74
+ const m = currentMatch();
75
+ if (m && m.id !== currentSessionId) {
76
+ onDelete(m.id);
77
+ setHint(`Deleted session ${m.id.slice(0, 8)}`);
78
+ }
79
+ return;
80
+ }
81
+ if (input === 'e') {
82
+ const m = currentMatch();
83
+ if (m) {
84
+ onExport(m.id);
85
+ setHint(`Exported ${m.id.slice(0, 8)} to JSON`);
86
+ }
87
+ return;
88
+ }
89
+ if (input === 't') {
90
+ const m = currentMatch();
91
+ if (m) {
92
+ onTranscript(m.id);
93
+ setHint(`Transcript of ${m.id.slice(0, 8)} written`);
94
+ }
95
+ return;
96
+ }
97
+ if (input === 'r') {
98
+ const m = currentMatch();
99
+ if (m) {
100
+ setRenameValue(m.title ?? '');
101
+ setRenameMode(true);
102
+ }
103
+ return;
104
+ }
105
+ }
106
+ if (key.backspace || key.delete) {
107
+ setQuery((q) => q.slice(0, -1));
108
+ setSelected(0);
109
+ }
110
+ else if (input && !key.ctrl && !key.meta && input >= ' ') {
111
+ setQuery((q) => q + input);
112
+ setSelected(0);
113
+ }
114
+ });
115
+ const half = Math.floor(MAX_VISIBLE / 2);
116
+ const start = matches.length > MAX_VISIBLE
117
+ ? Math.max(0, Math.min(selected - half, matches.length - MAX_VISIBLE))
118
+ : 0;
119
+ const visible = matches.slice(start, start + MAX_VISIBLE);
120
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.purple, paddingLeft: 1, paddingRight: 1, children: [_jsx(Text, { color: theme.purple, bold: true, children: "Sessions" }), _jsxs(Box, { children: [_jsx(Text, { color: theme.purple, bold: true, children: "\u276F " }), _jsx(Text, { color: theme.fg, children: query }), _jsxs(Text, { color: theme.fgDim, children: [" ", matches.length > 0 ? '(↑↓ select · Enter resume · type to search · d/e/t/r delete/export/transcript/rename · Esc)' : '— no matches'] })] }), hint ? _jsxs(Text, { color: theme.yellow, children: [" ", hint] }) : null, visible.length === 0 ? (_jsx(Text, { color: theme.fgDim, children: " No saved sessions yet. Chat to create one." })) : (visible.map((s, i) => {
121
+ const absIdx = start + i;
122
+ const sel = absIdx === selected;
123
+ const isCurrent = s.id === currentSessionId;
124
+ const label = s.title ?? truncate(s.preview, PREVIEW_LEN);
125
+ const subLabel = s.title ? truncate(s.preview, PREVIEW_LEN) : null;
126
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: _jsxs(Text, { backgroundColor: sel ? theme.blue : undefined, color: sel ? theme.bg : isCurrent ? theme.green : theme.fg, children: [sel ? '▶ ' : ' ', isCurrent ? '✓ ' : ' ', label] }) }), subLabel ? (_jsxs(Text, { color: theme.fgDim, children: [" ", subLabel] })) : null, _jsxs(Text, { color: theme.fgDim, children: [" ", relativeTime(s.updatedAt), " \u00B7 ", s.userMessageCount, " turns \u00B7 ", s.toolCallCount, " tools", s.model ? ` · ${s.model}` : ''] })] }, s.id));
127
+ })), matches.length > MAX_VISIBLE ? (_jsxs(Text, { color: theme.fgDim, children: [" ", matches.length - MAX_VISIBLE, " more"] })) : null] }));
128
+ }
129
+ function truncate(s, n) {
130
+ const oneLine = s.split('\n')[0].trim();
131
+ return oneLine.length > n ? oneLine.slice(0, n - 1) + '…' : oneLine;
132
+ }
133
+ /** Do query's chars appear in order within text? (empty query matches all) */
134
+ function isSubsequence(text, query) {
135
+ if (!query)
136
+ return true;
137
+ let qi = 0;
138
+ for (let i = 0; i < text.length && qi < query.length; i++) {
139
+ if (text[i] === query[qi])
140
+ qi++;
141
+ }
142
+ return qi === query.length;
143
+ }
144
+ /** Compact relative time, e.g. "2h ago", "3d ago". No deps. */
145
+ function relativeTime(epochMs) {
146
+ const sec = Math.floor((Date.now() - epochMs) / 1000);
147
+ if (sec < 60)
148
+ return 'just now';
149
+ const min = Math.floor(sec / 60);
150
+ if (min < 60)
151
+ return `${min}m ago`;
152
+ const hr = Math.floor(min / 60);
153
+ if (hr < 24)
154
+ return `${hr}h ago`;
155
+ const day = Math.floor(hr / 24);
156
+ if (day < 30)
157
+ return `${day}d ago`;
158
+ const mo = Math.floor(day / 30);
159
+ if (mo < 12)
160
+ return `${mo}mo ago`;
161
+ return `${Math.floor(mo / 12)}y ago`;
162
+ }
@@ -0,0 +1,24 @@
1
+ export interface SettingItem {
2
+ dotKey: string;
3
+ value: string;
4
+ category: string;
5
+ label: string;
6
+ type: 'string' | 'number' | 'boolean' | 'enum';
7
+ secret: boolean;
8
+ enumValues?: string[];
9
+ restartRequired: boolean;
10
+ }
11
+ interface SettingsEditorProps {
12
+ settings: SettingItem[];
13
+ onSet: (dotKey: string, value: string) => Promise<void>;
14
+ onClose: () => void;
15
+ }
16
+ /**
17
+ * Type-aware settings editor.
18
+ * - browse: ↑/↓ navigate categorized list; Enter edits.
19
+ * - select (boolean/enum): ←/→ cycle through options; Enter saves.
20
+ * - input (string/number/secret): type a value; Enter saves.
21
+ * After saving, the caller refreshes the list so the new value is visible.
22
+ */
23
+ export declare function SettingsEditor({ settings, onSet, onClose }: SettingsEditorProps): import("react").JSX.Element;
24
+ export {};
@@ -0,0 +1,126 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import { useTheme } from '../hooks/use-theme.js';
5
+ const MAX_VISIBLE = 12;
6
+ /** Options for a setting (boolean → ['true','false']; enum → enumValues). */
7
+ function optionsFor(s) {
8
+ if (s.type === 'boolean')
9
+ return ['true', 'false'];
10
+ return s.enumValues ?? [];
11
+ }
12
+ /**
13
+ * Type-aware settings editor.
14
+ * - browse: ↑/↓ navigate categorized list; Enter edits.
15
+ * - select (boolean/enum): ←/→ cycle through options; Enter saves.
16
+ * - input (string/number/secret): type a value; Enter saves.
17
+ * After saving, the caller refreshes the list so the new value is visible.
18
+ */
19
+ export function SettingsEditor({ settings, onSet, onClose }) {
20
+ const theme = useTheme();
21
+ const [mode, setMode] = useState('browse');
22
+ const [selected, setSelected] = useState(0);
23
+ const [inputVal, setInputVal] = useState('');
24
+ const [optIdx, setOptIdx] = useState(0);
25
+ const [feedback, setFeedback] = useState('');
26
+ const cur = settings[selected];
27
+ const enterEdit = () => {
28
+ if (!cur)
29
+ return;
30
+ setFeedback('');
31
+ if (cur.type === 'boolean' || cur.type === 'enum') {
32
+ const opts = optionsFor(cur);
33
+ const idx = opts.indexOf(cur.value);
34
+ setOptIdx(idx >= 0 ? idx : 0);
35
+ setMode('select');
36
+ }
37
+ else {
38
+ setInputVal(cur.value === '(not set)' || cur.value === '******' ? '' : cur.value);
39
+ setMode('input');
40
+ }
41
+ };
42
+ const save = (value) => {
43
+ if (!cur)
44
+ return;
45
+ void onSet(cur.dotKey, value)
46
+ .then(() => { setFeedback(`✓ ${cur.label} saved${cur.restartRequired ? ' (restart required)' : ''}`); })
47
+ .catch(() => { setFeedback(`✗ Failed to save ${cur.label}`); });
48
+ setMode('browse');
49
+ };
50
+ useInput((input, key) => {
51
+ if (mode === 'browse') {
52
+ if (key.escape) {
53
+ onClose();
54
+ return;
55
+ }
56
+ if (key.return) {
57
+ enterEdit();
58
+ return;
59
+ }
60
+ if (key.upArrow) {
61
+ setSelected((i) => Math.max(0, i - 1));
62
+ setFeedback('');
63
+ }
64
+ if (key.downArrow) {
65
+ setSelected((i) => Math.min(settings.length - 1, i + 1));
66
+ setFeedback('');
67
+ }
68
+ return;
69
+ }
70
+ if (mode === 'select') {
71
+ if (key.escape) {
72
+ setMode('browse');
73
+ return;
74
+ }
75
+ if (key.return) {
76
+ save(optionsFor(cur)[optIdx]);
77
+ return;
78
+ }
79
+ if (key.leftArrow || key.upArrow)
80
+ setOptIdx((i) => Math.max(0, i - 1));
81
+ if (key.rightArrow || key.downArrow)
82
+ setOptIdx((i) => Math.min(optionsFor(cur).length - 1, i + 1));
83
+ return;
84
+ }
85
+ // input mode
86
+ if (key.escape) {
87
+ setMode('browse');
88
+ return;
89
+ }
90
+ if (key.return) {
91
+ save(inputVal);
92
+ return;
93
+ }
94
+ if (key.backspace || key.delete) {
95
+ setInputVal((v) => v.slice(0, -1));
96
+ return;
97
+ }
98
+ if (input && !key.ctrl && !key.meta && input.length >= 1 && input >= ' ' && !/\x1b?\[\d[\d;]*[~A-Za-z]/.test(input)) {
99
+ setInputVal((v) => v + input);
100
+ }
101
+ });
102
+ // ── Select mode (boolean / enum) ──────────────────────────────────────
103
+ if (mode === 'select' && cur) {
104
+ const opts = optionsFor(cur);
105
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.cyan, paddingLeft: 1, paddingRight: 1, children: [_jsx(Text, { color: theme.cyan, bold: true, children: cur.label }), _jsx(Text, { color: theme.fgDim, children: cur.dotKey }), _jsx(Box, { marginTop: 1, children: opts.map((o, i) => (_jsx(Box, { children: _jsx(Text, { backgroundColor: i === optIdx ? theme.blue : undefined, color: i === optIdx ? theme.bg : i === opts.indexOf(cur.value) ? theme.green : theme.fgDim, children: i === optIdx ? ` ◉ ${o} ` : ` ○ ${o} ` }) }, o))) }), _jsxs(Text, { color: theme.fgDim, children: ["\u2190/\u2192 change \u00B7 Enter save \u00B7 Esc cancel", cur.restartRequired ? ' · ⚠ restart required' : ''] })] }));
106
+ }
107
+ // ── Input mode (string / number / secret) ─────────────────────────────
108
+ if (mode === 'input' && cur) {
109
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.cyan, paddingLeft: 1, paddingRight: 1, children: [_jsx(Text, { color: theme.cyan, bold: true, children: cur.label }), _jsx(Text, { color: theme.fgDim, children: cur.dotKey }), _jsxs(Text, { color: theme.fgDim, children: ["Current: ", cur.value] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.green, bold: true, children: "\u203A " }), _jsx(Text, { color: cur.secret ? theme.yellow : theme.fg, children: cur.secret ? '•'.repeat(inputVal.length) : inputVal }), _jsx(Text, { color: theme.fgDim, children: " _" })] }), _jsxs(Text, { color: theme.fgDim, children: ["Enter save \u00B7 Esc cancel", cur.restartRequired ? ' · ⚠ restart required' : ''] })] }));
110
+ }
111
+ // ── Browse mode ───────────────────────────────────────────────────────
112
+ const half = Math.floor(MAX_VISIBLE / 2);
113
+ const start = Math.max(0, Math.min(selected - half, settings.length - MAX_VISIBLE));
114
+ const visible = settings.slice(start, start + MAX_VISIBLE);
115
+ let lastCat = start > 0 ? settings[start - 1]?.category : null;
116
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.purple, paddingLeft: 1, paddingRight: 1, children: [_jsx(Text, { color: theme.purple, bold: true, children: "Settings" }), _jsx(Text, { color: theme.fgDim, children: "\u2191/\u2193 navigate \u00B7 Enter edit \u00B7 Esc close" }), feedback ? _jsx(Text, { color: feedback.startsWith('✓') ? theme.green : theme.red, children: feedback }) : null, visible.map((s) => {
117
+ const idx = settings.indexOf(s);
118
+ const sel = idx === selected;
119
+ const showCat = s.category !== lastCat;
120
+ lastCat = s.category;
121
+ const valDisplay = s.type === 'boolean'
122
+ ? (s.value === 'true' ? '✓ on' : '✗ off')
123
+ : s.value;
124
+ return (_jsxs(Box, { flexDirection: "column", children: [showCat ? _jsxs(Text, { color: theme.cyan, bold: true, children: [" ", s.category] }) : null, _jsxs(Box, { children: [_jsxs(Text, { backgroundColor: sel ? theme.blue : undefined, color: sel ? theme.bg : theme.fg, children: [sel ? '▶ ' : ' ', s.label] }), _jsx(Text, { backgroundColor: sel ? theme.blue : undefined, color: sel ? theme.bg : theme.fgDim, children: sel ? ` = ${valDisplay}` : ` ${valDisplay}` }), s.restartRequired ? _jsx(Text, { color: theme.yellow, children: " \u26A0" }) : null] })] }, s.dotKey));
125
+ }), settings.length > MAX_VISIBLE ? (_jsxs(Text, { color: theme.fgDim, children: [" ", start + 1, "\u2013", Math.min(start + MAX_VISIBLE, settings.length), " of ", settings.length] })) : null] }));
126
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * session-export — format a persisted session for export.
3
+ *
4
+ * Two formats:
5
+ * - `formatJson` — the full SessionData as pretty-printed JSON (lossless,
6
+ * machine-readable, re-importable).
7
+ * - `formatTranscript` — a human-readable Markdown transcript rendering each
8
+ * message with its role, tool-call names/args, and results.
9
+ *
10
+ * Both are pure functions over SessionData — no I/O, no deps — so they're
11
+ * trivially testable.
12
+ */
13
+ import type { SessionData } from '../../../core/types.js';
14
+ /** Lossless JSON dump of the full session record. */
15
+ export declare function formatJson(session: SessionData): string;
16
+ /**
17
+ * Human-readable Markdown transcript. Tool calls within an assistant message
18
+ * render as sub-sections; standalone role:"tool" results are joined by
19
+ * toolCallId (the same join the feed-serializer does for the TUI display).
20
+ */
21
+ export declare function formatTranscript(session: SessionData): string;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * session-export — format a persisted session for export.
3
+ *
4
+ * Two formats:
5
+ * - `formatJson` — the full SessionData as pretty-printed JSON (lossless,
6
+ * machine-readable, re-importable).
7
+ * - `formatTranscript` — a human-readable Markdown transcript rendering each
8
+ * message with its role, tool-call names/args, and results.
9
+ *
10
+ * Both are pure functions over SessionData — no I/O, no deps — so they're
11
+ * trivially testable.
12
+ */
13
+ /** Lossless JSON dump of the full session record. */
14
+ export function formatJson(session) {
15
+ return JSON.stringify(session, null, 2);
16
+ }
17
+ /**
18
+ * Human-readable Markdown transcript. Tool calls within an assistant message
19
+ * render as sub-sections; standalone role:"tool" results are joined by
20
+ * toolCallId (the same join the feed-serializer does for the TUI display).
21
+ */
22
+ export function formatTranscript(session) {
23
+ const created = new Date(session.createdAt).toISOString();
24
+ const lines = [
25
+ `# Session ${session.id.slice(0, 8)}`,
26
+ '',
27
+ `Created: ${created}`,
28
+ `Messages: ${session.messages.length}`,
29
+ session.provider ? `Provider: ${session.provider}` : '',
30
+ session.model ? `Model: ${session.model}` : '',
31
+ '',
32
+ '---',
33
+ '',
34
+ ].filter((l) => l !== '' || true);
35
+ // Index tool results by toolCallId for rendering alongside the call.
36
+ const toolResults = new Map();
37
+ for (const m of session.messages) {
38
+ if (m.role === 'tool' && m.toolCallId)
39
+ toolResults.set(m.toolCallId, m.content);
40
+ }
41
+ for (const m of session.messages) {
42
+ if (m.role === 'system')
43
+ continue;
44
+ if (m.role === 'user') {
45
+ lines.push('## User', '', m.content, '');
46
+ }
47
+ else if (m.role === 'assistant') {
48
+ if (m.content)
49
+ lines.push('## Assistant', '', m.content, '');
50
+ if (m.toolCalls) {
51
+ for (const tc of m.toolCalls) {
52
+ const result = tc.result ?? toolResults.get(tc.id);
53
+ lines.push(`### Tool: ${tc.name}`, '');
54
+ lines.push('```json', JSON.stringify(tc.arguments, null, 2), '```', '');
55
+ if (result != null) {
56
+ lines.push('**Result:**', '', '```', result, '```', '');
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
62
+ return lines.join('\n');
63
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Tokyo Night Moon — color palette for the Zoe TUI.
3
+ *
4
+ * US1 imports these tokens directly (`import { theme } from '../theme.js'`);
5
+ * US3 wraps them in a React context via `use-theme.ts` so components can be
6
+ * re-themed without touching call sites. Hex values are the single source of
7
+ * truth for every component color — no inline hex elsewhere.
8
+ */
9
+ export interface Theme {
10
+ bg: string;
11
+ bgHighlight: string;
12
+ fg: string;
13
+ fgDim: string;
14
+ fgGutter: string;
15
+ blue: string;
16
+ cyan: string;
17
+ green: string;
18
+ yellow: string;
19
+ red: string;
20
+ purple: string;
21
+ orange: string;
22
+ }
23
+ export declare const theme: Theme;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Tokyo Night Moon — color palette for the Zoe TUI.
3
+ *
4
+ * US1 imports these tokens directly (`import { theme } from '../theme.js'`);
5
+ * US3 wraps them in a React context via `use-theme.ts` so components can be
6
+ * re-themed without touching call sites. Hex values are the single source of
7
+ * truth for every component color — no inline hex elsewhere.
8
+ */
9
+ export const theme = {
10
+ bg: '#222436',
11
+ bgHighlight: '#2f334d',
12
+ fg: '#c8d3f5',
13
+ fgDim: '#828bb8',
14
+ fgGutter: '#3b4261',
15
+ blue: '#82aaff',
16
+ cyan: '#86e1fc',
17
+ green: '#c3e88d',
18
+ yellow: '#ffc777',
19
+ red: '#ff757f',
20
+ purple: '#c099ff',
21
+ orange: '#ff966c',
22
+ };