soja-cli 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +117 -0
  3. package/dist/application/chat.js +1 -0
  4. package/dist/application/filters.js +27 -0
  5. package/dist/application/import.js +1 -0
  6. package/dist/application/ports.js +1 -0
  7. package/dist/application/repository-root.js +18 -0
  8. package/dist/application/services/backup-service.js +93 -0
  9. package/dist/application/services/folder-service.js +116 -0
  10. package/dist/application/services/git-workflow-service.js +593 -0
  11. package/dist/application/services/index.js +24 -0
  12. package/dist/application/services/project-service.js +129 -0
  13. package/dist/application/services/session-service.js +52 -0
  14. package/dist/application/services/task-service.js +350 -0
  15. package/dist/application/services/update-service.js +60 -0
  16. package/dist/application/services/workspace-service.js +81 -0
  17. package/dist/application/timeline.js +83 -0
  18. package/dist/application/types.js +1 -0
  19. package/dist/application/validation.js +30 -0
  20. package/dist/bootstrap.js +91 -0
  21. package/dist/cli/commands/access.js +60 -0
  22. package/dist/cli/commands/account.js +203 -0
  23. package/dist/cli/commands/args.js +27 -0
  24. package/dist/cli/commands/backup.js +70 -0
  25. package/dist/cli/commands/chat.js +133 -0
  26. package/dist/cli/commands/dev.js +59 -0
  27. package/dist/cli/commands/folders.js +59 -0
  28. package/dist/cli/commands/git-ops.js +200 -0
  29. package/dist/cli/commands/import.js +48 -0
  30. package/dist/cli/commands/info.js +82 -0
  31. package/dist/cli/commands/project.js +108 -0
  32. package/dist/cli/commands/start.js +34 -0
  33. package/dist/cli/commands/sync.js +43 -0
  34. package/dist/cli/commands/task.js +309 -0
  35. package/dist/cli/commands/tui.js +43 -0
  36. package/dist/cli/commands/update.js +20 -0
  37. package/dist/cli/commands/workspace.js +55 -0
  38. package/dist/cli/index.js +103 -0
  39. package/dist/cli/output.js +56 -0
  40. package/dist/cli/prompt.js +28 -0
  41. package/dist/cli/runtime.js +43 -0
  42. package/dist/config/config.js +103 -0
  43. package/dist/config/credentials.js +68 -0
  44. package/dist/config/paths.js +18 -0
  45. package/dist/config/server.js +21 -0
  46. package/dist/config/version.js +7 -0
  47. package/dist/data/local/activity.js +33 -0
  48. package/dist/data/local/clock.js +1 -0
  49. package/dist/data/local/comments.js +27 -0
  50. package/dist/data/local/index.js +48 -0
  51. package/dist/data/local/projects.js +53 -0
  52. package/dist/data/local/tasks.js +104 -0
  53. package/dist/data/local/users.js +38 -0
  54. package/dist/data/local/workspaces.js +55 -0
  55. package/dist/data/remote/api-client.js +103 -0
  56. package/dist/data/remote/live.js +93 -0
  57. package/dist/data/repositories.js +1 -0
  58. package/dist/data/sync/chat.js +242 -0
  59. package/dist/data/sync/engine.js +459 -0
  60. package/dist/data/sync/importer.js +156 -0
  61. package/dist/data/sync/index.js +109 -0
  62. package/dist/data/sync/services.js +263 -0
  63. package/dist/data/sync/store.js +200 -0
  64. package/dist/database/client.js +76 -0
  65. package/dist/database/migrate.js +27 -0
  66. package/dist/database/migrations/0000_init.sql +100 -0
  67. package/dist/database/migrations/0001_git-operations.sql +19 -0
  68. package/dist/database/migrations/0002_merge-detection.sql +19 -0
  69. package/dist/database/migrations/0003_sync-replica.sql +40 -0
  70. package/dist/database/migrations/0004_chat-replica.sql +52 -0
  71. package/dist/database/migrations/0005_pr-events.sql +18 -0
  72. package/dist/database/migrations/0006_archive.sql +20 -0
  73. package/dist/database/migrations/meta/0000_snapshot.json +710 -0
  74. package/dist/database/migrations/meta/0001_snapshot.json +717 -0
  75. package/dist/database/migrations/meta/0002_snapshot.json +724 -0
  76. package/dist/database/migrations/meta/0003_snapshot.json +972 -0
  77. package/dist/database/migrations/meta/0004_snapshot.json +1168 -0
  78. package/dist/database/migrations/meta/0005_snapshot.json +1168 -0
  79. package/dist/database/migrations/meta/0006_snapshot.json +1183 -0
  80. package/dist/database/migrations/meta/_journal.json +55 -0
  81. package/dist/database/schema.js +170 -0
  82. package/dist/dev/seed.js +84 -0
  83. package/dist/domain/activity.js +22 -0
  84. package/dist/domain/chat.js +21 -0
  85. package/dist/domain/entities.js +1 -0
  86. package/dist/domain/errors.js +23 -0
  87. package/dist/domain/naming.js +33 -0
  88. package/dist/domain/task.js +140 -0
  89. package/dist/domain/workflow.js +23 -0
  90. package/dist/git/cli-git.js +363 -0
  91. package/dist/git/console.js +37 -0
  92. package/dist/git/diagnose.js +145 -0
  93. package/dist/git/merge-evidence.js +23 -0
  94. package/dist/git/releases.js +95 -0
  95. package/dist/git/types.js +16 -0
  96. package/dist/ui/App.js +71 -0
  97. package/dist/ui/Shell.js +211 -0
  98. package/dist/ui/app-state.js +213 -0
  99. package/dist/ui/branding/CompactLogo.js +8 -0
  100. package/dist/ui/branding/Logo.js +22 -0
  101. package/dist/ui/branding/Splash.js +21 -0
  102. package/dist/ui/branding/brand.js +23 -0
  103. package/dist/ui/chrome/Sidebar.js +50 -0
  104. package/dist/ui/chrome/StatusBar.js +26 -0
  105. package/dist/ui/chrome/Toast.js +11 -0
  106. package/dist/ui/chrome/TopBar.js +10 -0
  107. package/dist/ui/chrome/context.js +33 -0
  108. package/dist/ui/components/ConsoleLines.js +16 -0
  109. package/dist/ui/components/EmptyState.js +8 -0
  110. package/dist/ui/components/Labels.js +25 -0
  111. package/dist/ui/components/PullRequestLabel.js +27 -0
  112. package/dist/ui/components/TaskTable.js +41 -0
  113. package/dist/ui/components/TextField.js +11 -0
  114. package/dist/ui/components/TextInput.js +29 -0
  115. package/dist/ui/components/task-columns.js +27 -0
  116. package/dist/ui/copy.js +23 -0
  117. package/dist/ui/hooks/use-commands.js +58 -0
  118. package/dist/ui/hooks/use-flows.js +254 -0
  119. package/dist/ui/hooks/use-layout.js +36 -0
  120. package/dist/ui/hooks/use-list.js +28 -0
  121. package/dist/ui/hooks/use-measure.js +22 -0
  122. package/dist/ui/hooks/use-query.js +29 -0
  123. package/dist/ui/hooks/use-task-actions.js +450 -0
  124. package/dist/ui/hooks/use-terminal-size.js +19 -0
  125. package/dist/ui/input/KeyProvider.js +101 -0
  126. package/dist/ui/input/dispatcher.js +26 -0
  127. package/dist/ui/input/list-navigation.js +38 -0
  128. package/dist/ui/input/mouse.js +78 -0
  129. package/dist/ui/input/text-editing.js +62 -0
  130. package/dist/ui/kit/Badge.js +8 -0
  131. package/dist/ui/kit/Button.js +13 -0
  132. package/dist/ui/kit/Clickable.js +14 -0
  133. package/dist/ui/kit/Gauge.js +9 -0
  134. package/dist/ui/kit/Keycap.js +7 -0
  135. package/dist/ui/kit/Panel.js +30 -0
  136. package/dist/ui/kit/ScrollBar.js +14 -0
  137. package/dist/ui/kit/Spinner.js +9 -0
  138. package/dist/ui/kit/Tabs.js +12 -0
  139. package/dist/ui/mascot/AgentScene.js +30 -0
  140. package/dist/ui/mascot/Mascot.js +26 -0
  141. package/dist/ui/mascot/ansi.js +25 -0
  142. package/dist/ui/mascot/engine.js +146 -0
  143. package/dist/ui/mascot/index.js +6 -0
  144. package/dist/ui/mascot/palette.js +18 -0
  145. package/dist/ui/mascot/scenes/bugHunt.js +115 -0
  146. package/dist/ui/mascot/scenes/celebrate.js +76 -0
  147. package/dist/ui/mascot/scenes/cleaning.js +156 -0
  148. package/dist/ui/mascot/scenes/clockIn.js +105 -0
  149. package/dist/ui/mascot/scenes/deadline.js +125 -0
  150. package/dist/ui/mascot/scenes/empty.js +149 -0
  151. package/dist/ui/mascot/scenes/fire.js +138 -0
  152. package/dist/ui/mascot/scenes/index.js +25 -0
  153. package/dist/ui/mascot/scenes/planning.js +73 -0
  154. package/dist/ui/mascot/scenes/resting.js +90 -0
  155. package/dist/ui/mascot/scenes/syncing.js +116 -0
  156. package/dist/ui/mascot/scenes/working.js +193 -0
  157. package/dist/ui/mascot/selection.js +32 -0
  158. package/dist/ui/mascot/states.js +25 -0
  159. package/dist/ui/navigation/routes.js +17 -0
  160. package/dist/ui/overlays/CommitOverlay.js +93 -0
  161. package/dist/ui/overlays/ConfirmOverlay.js +42 -0
  162. package/dist/ui/overlays/GitLogOverlay.js +22 -0
  163. package/dist/ui/overlays/GitRunOverlay.js +94 -0
  164. package/dist/ui/overlays/NewTaskOverlay.js +117 -0
  165. package/dist/ui/overlays/OverlayFrame.js +35 -0
  166. package/dist/ui/overlays/PickerOverlay.js +72 -0
  167. package/dist/ui/overlays/PromptOverlay.js +51 -0
  168. package/dist/ui/overlays/SearchOverlay.js +49 -0
  169. package/dist/ui/overlays/options.js +29 -0
  170. package/dist/ui/overlays/types.js +1 -0
  171. package/dist/ui/screens/AccessScreen.js +91 -0
  172. package/dist/ui/screens/ChatScreen.js +450 -0
  173. package/dist/ui/screens/HelpScreen.js +99 -0
  174. package/dist/ui/screens/ProjectsScreen.js +66 -0
  175. package/dist/ui/screens/ScreenFrame.js +14 -0
  176. package/dist/ui/screens/SetupScreen.js +79 -0
  177. package/dist/ui/screens/TaskListScreen.js +152 -0
  178. package/dist/ui/screens/TaskScreen.js +193 -0
  179. package/dist/ui/screens/WelcomeScreen.js +254 -0
  180. package/dist/ui/screens/WorkspacesScreen.js +64 -0
  181. package/dist/ui/theme/detect.js +59 -0
  182. package/dist/ui/theme/theme.js +135 -0
  183. package/dist/utils/editor.js +37 -0
  184. package/dist/utils/errors.js +23 -0
  185. package/dist/utils/text.js +60 -0
  186. package/dist/utils/time.js +25 -0
  187. package/docs/NPM_PUBLISH.md +35 -0
  188. package/docs/PRIVACY.md +38 -0
  189. package/package.json +74 -0
@@ -0,0 +1,101 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useApp, useInput, useStdout } from 'ink';
3
+ import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useRef, useState } from 'react';
4
+ import { KeyDispatcher, Layer } from './dispatcher.js';
5
+ import { MOUSE_OFF, MOUSE_ON, MouseDispatcher, parseMouse, rectOf } from './mouse.js';
6
+ const InputContext = createContext(null);
7
+ /**
8
+ * All input goes through here: keys to the layered key dispatcher, mouse
9
+ * reports to the clickable regions. A mouse report never reaches a key handler.
10
+ */
11
+ export function KeyProvider({ children, mouse = false }) {
12
+ const [context] = useState(() => ({ keys: new KeyDispatcher(), mouse: new MouseDispatcher(), mouseEnabled: mouse }));
13
+ const { stdout } = useStdout();
14
+ useInput((input, key) => {
15
+ const event = parseMouse(input);
16
+ if (event)
17
+ context.mouse.dispatch(event);
18
+ else
19
+ context.keys.dispatch(input, key);
20
+ });
21
+ useEffect(() => {
22
+ if (!mouse)
23
+ return;
24
+ stdout.write(MOUSE_ON);
25
+ // Whatever way SOJA ends, the terminal must stop sending mouse reports.
26
+ const off = () => stdout.write(MOUSE_OFF);
27
+ process.once('exit', off);
28
+ return () => {
29
+ off();
30
+ process.off('exit', off);
31
+ };
32
+ }, [mouse, stdout]);
33
+ return _jsx(InputContext.Provider, { value: context, children: children });
34
+ }
35
+ function useInputContext() {
36
+ const context = useContext(InputContext);
37
+ if (!context)
38
+ throw new Error('Input hooks must be used inside <KeyProvider>');
39
+ return context;
40
+ }
41
+ /**
42
+ * Registers `handler` on a layer while `active`. The latest handler is always used.
43
+ * Layout effects register during the commit itself, so a key typed right after
44
+ * opening an overlay reaches the overlay, not the shortcut underneath it.
45
+ */
46
+ export function useKeys(layer, handler, active = true) {
47
+ const { keys } = useInputContext();
48
+ const latest = useRef(handler);
49
+ useLayoutEffect(() => {
50
+ latest.current = handler;
51
+ });
52
+ useLayoutEffect(() => {
53
+ if (!active)
54
+ return;
55
+ return keys.register(layer, (input, key) => latest.current(input, key));
56
+ }, [keys, layer, active]);
57
+ }
58
+ /**
59
+ * Makes the element behind `ref` react to clicks and the wheel while `active`.
60
+ * Its position is read from the layout when the event arrives, so it follows
61
+ * scrolling and resizing.
62
+ */
63
+ export function useClickable(ref, target, options = {}) {
64
+ const { mouse } = useInputContext();
65
+ const latest = useRef(target);
66
+ useLayoutEffect(() => {
67
+ latest.current = target;
68
+ });
69
+ const layer = options.layer ?? Layer.screen;
70
+ const active = options.active ?? true;
71
+ useLayoutEffect(() => {
72
+ if (!active)
73
+ return;
74
+ return mouse.register(layer, () => rectOf(ref.current), () => latest.current);
75
+ }, [mouse, layer, active, ref]);
76
+ }
77
+ /**
78
+ * Hands the terminal to another program (an editor, Git asking for a
79
+ * password) and takes it back. The mouse is switched off meanwhile, so the
80
+ * program does not receive mouse reports as typed text.
81
+ */
82
+ export function useExternalTerminal() {
83
+ const { suspendTerminal } = useApp();
84
+ const { stdout } = useStdout();
85
+ const { mouseEnabled } = useInputContext();
86
+ return useCallback(async (work) => {
87
+ let result;
88
+ if (mouseEnabled)
89
+ stdout.write(MOUSE_OFF);
90
+ try {
91
+ await suspendTerminal(async () => {
92
+ result = await work();
93
+ });
94
+ }
95
+ finally {
96
+ if (mouseEnabled)
97
+ stdout.write(MOUSE_ON);
98
+ }
99
+ return result;
100
+ }, [suspendTerminal, stdout, mouseEnabled]);
101
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Who gets a key first. Text fields beat overlays, overlays beat screens,
3
+ * screens beat global shortcuts. That ordering is what lets `p` mean
4
+ * "priority" in a task and "projects" everywhere else, and keeps Esc
5
+ * predictable: the topmost thing always closes first.
6
+ */
7
+ export const Layer = { global: 0, screen: 10, overlay: 20, input: 30 };
8
+ export class KeyDispatcher {
9
+ entries = [];
10
+ counter = 0;
11
+ register(layer, handler) {
12
+ const entry = { layer, order: (this.counter += 1), handler };
13
+ this.entries.push(entry);
14
+ return () => {
15
+ this.entries = this.entries.filter((candidate) => candidate !== entry);
16
+ };
17
+ }
18
+ dispatch(input, key) {
19
+ const ordered = [...this.entries].sort((a, b) => b.layer - a.layer || b.order - a.order);
20
+ for (const entry of ordered) {
21
+ if (entry.handler(input, key) === true)
22
+ return true;
23
+ }
24
+ return false;
25
+ }
26
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Vim-style movement over a list of `count` items shown `height` at a time.
3
+ * Returns the new state, or `null` when the key is not a movement key.
4
+ * `vim` is off inside text inputs, where j/k are letters.
5
+ */
6
+ export function moveInList(state, input, key, count, height, vim = true) {
7
+ // Letters only count as vim motions when typed bare: ctrl+k is not k.
8
+ const letter = vim && !key.ctrl && !key.meta ? input : '';
9
+ let index;
10
+ if (key.downArrow || letter === 'j' || (key.ctrl && input === 'n'))
11
+ index = state.index + 1;
12
+ else if (key.upArrow || letter === 'k' || (key.ctrl && input === 'p'))
13
+ index = state.index - 1;
14
+ else if (key.pageDown || (key.ctrl && input === 'd'))
15
+ index = state.index + Math.max(1, Math.floor(height / 2));
16
+ else if (key.pageUp || (key.ctrl && input === 'u'))
17
+ index = state.index - Math.max(1, Math.floor(height / 2));
18
+ else if (letter === 'g' || key.home)
19
+ index = 0;
20
+ else if (letter === 'G' || key.end)
21
+ index = count - 1;
22
+ else
23
+ return null;
24
+ return clampList({ index, offset: state.offset }, count, height);
25
+ }
26
+ /** Keeps the selection inside the list and the viewport around the selection. */
27
+ export function clampList(state, count, height) {
28
+ if (count <= 0)
29
+ return { index: 0, offset: 0 };
30
+ const visible = Math.max(1, height);
31
+ const index = Math.max(0, Math.min(count - 1, state.index));
32
+ let offset = Math.max(0, Math.min(state.offset, Math.max(0, count - visible)));
33
+ if (index < offset)
34
+ offset = index;
35
+ if (index >= offset + visible)
36
+ offset = index - visible + 1;
37
+ return { index, offset };
38
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * SGR mouse reports (`ESC [ < b ; x ; y M|m`). Ink strips the ESC and hands
3
+ * the rest to `useInput`, so this recognizes them there before they could be
4
+ * mistaken for keys.
5
+ */
6
+ const SGR = /^\[<(\d+);(\d+);(\d+)([Mm])$/;
7
+ export function parseMouse(input) {
8
+ const match = SGR.exec(input);
9
+ if (!match)
10
+ return null;
11
+ const code = Number(match[1]);
12
+ const x = Number(match[2]) - 1;
13
+ const y = Number(match[3]) - 1;
14
+ if (code & 64)
15
+ return { kind: (code & 1) === 1 ? 'wheel-down' : 'wheel-up', button: 'none', x, y };
16
+ const button = ['left', 'middle', 'right'][code & 3] ?? 'none';
17
+ return { kind: match[4] === 'M' ? 'press' : 'release', button, x, y };
18
+ }
19
+ /** Terminal sequences: report presses, releases and the wheel, in SGR format. */
20
+ export const MOUSE_ON = '\u001b[?1000h\u001b[?1006h';
21
+ export const MOUSE_OFF = '\u001b[?1000l\u001b[?1006l';
22
+ /** Where an Ink element is on screen: its layout offsets summed up to the root. */
23
+ export function rectOf(element) {
24
+ const layout = element?.yogaNode?.getComputedLayout();
25
+ if (!element || !layout)
26
+ return null;
27
+ let x = layout.left;
28
+ let y = layout.top;
29
+ for (let parent = element.parentNode; parent; parent = parent.parentNode) {
30
+ const offset = parent.yogaNode?.getComputedLayout();
31
+ if (offset) {
32
+ x += offset.left;
33
+ y += offset.top;
34
+ }
35
+ }
36
+ return { x: Math.round(x), y: Math.round(y), width: Math.round(layout.width), height: Math.round(layout.height) };
37
+ }
38
+ const contains = (rect, event) => event.x >= rect.x && event.x < rect.x + rect.width && event.y >= rect.y && event.y < rect.y + rect.height;
39
+ /**
40
+ * Clickable regions, by layer like the keyboard: an open window catches
41
+ * clicks before the screen under it. Within the top layer that contains the
42
+ * point, the smallest region wins (a button inside a panel).
43
+ */
44
+ export class MouseDispatcher {
45
+ regions = [];
46
+ counter = 0;
47
+ register(layer, rect, target) {
48
+ const region = { layer, order: (this.counter += 1), rect, target };
49
+ this.regions.push(region);
50
+ return () => {
51
+ this.regions = this.regions.filter((candidate) => candidate !== region);
52
+ };
53
+ }
54
+ dispatch(event) {
55
+ // Clicks act on press, like most terminal apps; releases are ignored.
56
+ if (event.kind === 'release')
57
+ return false;
58
+ const hits = this.regions
59
+ .map((region) => ({ region, rect: region.rect() }))
60
+ .filter((hit) => hit.rect !== null && contains(hit.rect, event));
61
+ const topLayer = Math.max(...hits.map((hit) => hit.region.layer));
62
+ const candidates = hits
63
+ .filter((hit) => hit.region.layer === topLayer)
64
+ .sort((a, b) => a.rect.width * a.rect.height - b.rect.width * b.rect.height || b.region.order - a.region.order);
65
+ for (const { region, rect } of candidates) {
66
+ const target = region.target();
67
+ if (event.kind === 'press' && event.button === 'left' && target.onClick) {
68
+ target.onClick(event, rect);
69
+ return true;
70
+ }
71
+ if ((event.kind === 'wheel-up' || event.kind === 'wheel-down') && target.onWheel) {
72
+ target.onWheel(event.kind === 'wheel-down' ? 1 : -1, event);
73
+ return true;
74
+ }
75
+ }
76
+ return false;
77
+ }
78
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Pure line-editing rules for text fields (readline-style). Returns the new
3
+ * state, or `null` when the key is not an editing key, so the owner can use
4
+ * it (Enter, Esc, Tab, arrows up/down).
5
+ */
6
+ export function editText(state, input, key) {
7
+ const { value, cursor } = state;
8
+ const at = (nextValue, nextCursor) => ({
9
+ value: nextValue,
10
+ cursor: Math.max(0, Math.min(nextValue.length, nextCursor)),
11
+ });
12
+ if (key.return || key.escape || key.tab || key.upArrow || key.downArrow || key.pageUp || key.pageDown)
13
+ return null;
14
+ if (key.leftArrow)
15
+ return at(value, key.ctrl || key.meta ? wordStart(value, cursor) : cursor - 1);
16
+ if (key.rightArrow)
17
+ return at(value, key.ctrl || key.meta ? wordEnd(value, cursor) : cursor + 1);
18
+ if (key.home || (key.ctrl && input === 'a'))
19
+ return at(value, 0);
20
+ if (key.end || (key.ctrl && input === 'e'))
21
+ return at(value, value.length);
22
+ if (key.backspace) {
23
+ if (key.meta) {
24
+ const start = wordStart(value, cursor);
25
+ return at(value.slice(0, start) + value.slice(cursor), start);
26
+ }
27
+ return cursor === 0 ? state : at(value.slice(0, cursor - 1) + value.slice(cursor), cursor - 1);
28
+ }
29
+ if (key.delete)
30
+ return at(value.slice(0, cursor) + value.slice(cursor + 1), cursor);
31
+ if (key.ctrl && input === 'u')
32
+ return at(value.slice(cursor), 0);
33
+ if (key.ctrl && input === 'k')
34
+ return at(value.slice(0, cursor), cursor);
35
+ if (key.ctrl && input === 'w') {
36
+ const start = wordStart(value, cursor);
37
+ return at(value.slice(0, start) + value.slice(cursor), start);
38
+ }
39
+ if (key.ctrl || key.meta)
40
+ return null;
41
+ // Printable text, including pastes. Newlines and control characters are dropped.
42
+ const text = input.replace(/[\r\n]+/g, ' ').replace(/\p{Cc}/gu, '');
43
+ if (!text)
44
+ return null;
45
+ return at(value.slice(0, cursor) + text + value.slice(cursor), cursor + text.length);
46
+ }
47
+ function wordStart(value, cursor) {
48
+ let index = cursor;
49
+ while (index > 0 && value[index - 1] === ' ')
50
+ index -= 1;
51
+ while (index > 0 && value[index - 1] !== ' ')
52
+ index -= 1;
53
+ return index;
54
+ }
55
+ function wordEnd(value, cursor) {
56
+ let index = cursor;
57
+ while (index < value.length && value[index] === ' ')
58
+ index += 1;
59
+ while (index < value.length && value[index] !== ' ')
60
+ index += 1;
61
+ return index;
62
+ }
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Text } from 'ink';
3
+ import { toneColors } from '../theme/theme.js';
4
+ /** A filled label: ` IN PROGRESS `. The soft background keeps it readable in light and dark terminals. */
5
+ export function Badge({ tone, children, bold = false }) {
6
+ const { fg, bg } = toneColors(tone);
7
+ return (_jsx(Text, { backgroundColor: bg, color: fg, bold: bold, children: ` ${children} ` }));
8
+ }
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Text } from 'ink';
3
+ import { palette, toneColors } from '../theme/theme.js';
4
+ import { Layer } from '../input/dispatcher.js';
5
+ import { Clickable } from './Clickable.js';
6
+ /** `[ Delete ]`: a clickable action in a window. `primary` is filled; `focused` shows which one Enter picks. */
7
+ export function Button({ label, onPress, variant = 'default', tone, focused = false, layer = Layer.overlay, }) {
8
+ const colors = tone ? toneColors(tone) : null;
9
+ const filled = variant === 'primary' || focused;
10
+ const background = filled ? (colors?.fg ?? palette.accent) : palette.neutralSoft;
11
+ const foreground = filled ? palette.onAccent : (colors?.fg ?? palette.text);
12
+ return (_jsx(Clickable, { onClick: onPress, layer: layer, children: _jsx(Text, { backgroundColor: background, color: foreground, bold: filled, children: ` ${label} ` }) }));
13
+ }
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box } from 'ink';
3
+ import { useRef } from 'react';
4
+ import { Layer } from '../input/dispatcher.js';
5
+ import { useClickable } from '../input/KeyProvider.js';
6
+ /** Wraps anything that reacts to the mouse. Inactive screens (hidden or under a window) ignore it. */
7
+ export function Clickable({ onClick, onWheel, layer = Layer.screen, active = true, children, ...box }) {
8
+ const ref = useRef(null);
9
+ useClickable(ref, {
10
+ ...(onClick ? { onClick } : {}),
11
+ ...(onWheel ? { onWheel: (direction) => onWheel(direction) } : {}),
12
+ }, { layer, active: active && Boolean(onClick || onWheel) });
13
+ return (_jsx(Box, { ref: ref, ...box, children: children }));
14
+ }
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Text } from 'ink';
3
+ import { palette, symbols } from '../theme/theme.js';
4
+ /** A thin progress bar: `████░░░░ 50%`. */
5
+ export function Gauge({ value, total, width = 10, color = palette.accent, showPercent = true }) {
6
+ const ratio = total > 0 ? Math.max(0, Math.min(1, value / total)) : 0;
7
+ const filled = Math.round(ratio * width);
8
+ return (_jsxs(Text, { children: [_jsx(Text, { color: color, children: symbols.full.repeat(filled) }), _jsx(Text, { color: palette.border, children: symbols.shade.repeat(width - filled) }), showPercent ? _jsx(Text, { color: palette.muted, children: ` ${String(Math.round(ratio * 100)).padStart(3)}%` }) : null] }));
9
+ }
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Text } from 'ink';
3
+ import { palette } from '../theme/theme.js';
4
+ /** A key drawn like a keyboard key, followed by what it does: `[ enter ] open`. */
5
+ export function Keycap({ keys, label }) {
6
+ return (_jsxs(Text, { children: [_jsx(Text, { backgroundColor: palette.neutralSoft, color: palette.text, bold: true, children: ` ${keys} ` }), label ? _jsx(Text, { color: palette.muted, children: ` ${label}` }) : null] }));
7
+ }
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { useRef } from 'react';
4
+ import { useMeasure } from '../hooks/use-measure.js';
5
+ import { useTerminalSize } from '../hooks/use-terminal-size.js';
6
+ import { palette } from '../theme/theme.js';
7
+ /**
8
+ * A rounded box with its title in the border, the basic block of every
9
+ * screen. The top border is drawn by hand so the title can sit in it.
10
+ */
11
+ export function Panel({ title, aside, focused = false, width, height, flexGrow, flexShrink, backgroundColor, paddingX = 1, children }) {
12
+ const ref = useRef(null);
13
+ // Re-render on resize so the drawn border follows the new width.
14
+ useTerminalSize();
15
+ const { width: measured } = useMeasure(ref);
16
+ const border = focused ? palette.borderFocus : palette.border;
17
+ return (_jsxs(Box, { ref: ref, flexDirection: "column", ...(width !== undefined ? { width } : {}), ...(height !== undefined ? { height } : {}), ...(flexGrow !== undefined ? { flexGrow } : {}), ...(flexShrink !== undefined ? { flexShrink } : {}), ...(backgroundColor ? { backgroundColor } : {}), children: [_jsx(Box, { width: "100%", height: 1, overflow: "hidden", flexShrink: 0, children: _jsx(TopBorder, { width: typeof width === 'number' ? width : measured, title: title, aside: aside, color: border, focused: focused, backgroundColor: backgroundColor }) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, ...(height !== undefined ? { height: Math.max(1, height - 1) } : {}), borderStyle: "round", borderTop: false, borderColor: border, paddingX: paddingX, overflow: "hidden", ...(backgroundColor ? { borderBottomBackgroundColor: backgroundColor, borderLeftBackgroundColor: backgroundColor, borderRightBackgroundColor: backgroundColor } : {}), children: _jsx(Box, { flexDirection: "column", flexShrink: 0, flexGrow: 1, children: children }) })] }));
18
+ }
19
+ function TopBorder({ width, title, aside, color, focused, backgroundColor, }) {
20
+ const inner = Math.max(0, width - 2);
21
+ const left = title ? ` ${title} ` : '';
22
+ const right = aside ? ` ${aside} ` : '';
23
+ // Keep "─" on both sides of the title; drop the aside first when narrow.
24
+ const fitsAside = left.length + right.length + 3 <= inner;
25
+ const shownRight = fitsAside ? right : '';
26
+ const shownLeft = left.length + 2 > inner ? left.slice(0, Math.max(0, inner - 3)) : left;
27
+ const fill = Math.max(0, inner - 1 - shownLeft.length - shownRight.length - (shownRight ? 1 : 0));
28
+ const background = backgroundColor ? { backgroundColor } : {};
29
+ return (_jsxs(Text, { wrap: "truncate", ...background, children: [_jsx(Text, { color: color, children: `╭─` }), shownLeft ? (_jsx(Text, { color: focused ? palette.accent : palette.text, bold: true, children: shownLeft })) : null, _jsx(Text, { color: color, children: '─'.repeat(fill) }), shownRight ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: palette.muted, children: shownRight }), _jsx(Text, { color: color, children: "\u2500" })] })) : null, _jsx(Text, { color: color, children: "\u256E" })] }));
30
+ }
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { palette } from '../theme/theme.js';
4
+ /** A one-column scroll indicator: where the visible rows are within the whole list. Nothing when it all fits. */
5
+ export function ScrollBar({ total, visible, offset, height }) {
6
+ if (total <= visible || height <= 0)
7
+ return null;
8
+ const thumb = Math.max(1, Math.round((visible / total) * height));
9
+ const start = Math.min(height - thumb, Math.round((offset / Math.max(1, total - visible)) * (height - thumb)));
10
+ return (_jsx(Box, { flexDirection: "column", width: 1, flexShrink: 0, children: Array.from({ length: height }, (_, row) => {
11
+ const inThumb = row >= start && row < start + thumb;
12
+ return (_jsx(Text, { color: inThumb ? palette.muted : palette.border, children: inThumb ? '┃' : '│' }, row));
13
+ }) }));
14
+ }
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Text, useAnimation } from 'ink';
3
+ import { palette } from '../theme/theme.js';
4
+ const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
5
+ /** Work in progress (syncing, talking to Git or GitHub). */
6
+ export function Spinner({ label, color = palette.accent }) {
7
+ const { frame } = useAnimation({ interval: 80 });
8
+ return (_jsxs(Text, { children: [_jsx(Text, { color: color, children: FRAMES[frame % FRAMES.length] }), label ? _jsx(Text, { color: palette.muted, children: ` ${label}` }) : null] }));
9
+ }
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { palette } from '../theme/theme.js';
4
+ import { Clickable } from './Clickable.js';
5
+ /** Filled pill for the active tab, muted text for the rest; each one clickable. */
6
+ export function Tabs({ items, value, onChange, active = true, compact = false, }) {
7
+ return (_jsx(Box, { gap: compact ? 0 : 1, flexWrap: "nowrap", children: items.map((item) => {
8
+ const selected = item.value === value;
9
+ const text = `${!compact && item.shortcut ? `${item.shortcut} ` : ''}${item.label}${item.count !== undefined ? ` ${item.count}` : ''}`;
10
+ return (_jsx(Clickable, { onClick: () => onChange(item.value), active: active, children: _jsx(Text, { ...(selected ? { backgroundColor: palette.accent, color: palette.onAccent, bold: true } : { color: palette.muted }), children: ` ${text} ` }) }, item.value));
11
+ }) }));
12
+ }
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { memo, useEffect, useMemo, useState } from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import { Canvas } from './engine.js';
5
+ import { SCENES } from './scenes/index.js';
6
+ import { defaultPalette } from './palette.js';
7
+ /**
8
+ * Escena ASCII animada de mini agentes para Ink.
9
+ *
10
+ * <AgentScene scene="working" />
11
+ */
12
+ export const AgentScene = memo(function AgentScene({ scene, fps = 10, paused = false, showStatus = true, statusText, palette, seed, }) {
13
+ const def = SCENES[scene];
14
+ const runtime = useMemo(() => def.create(seed), [def, seed]);
15
+ const colors = useMemo(() => ({ ...defaultPalette, ...palette }), [palette]);
16
+ const [, setFrame] = useState(0);
17
+ useEffect(() => {
18
+ if (paused)
19
+ return;
20
+ const id = setInterval(() => {
21
+ runtime.step();
22
+ setFrame((f) => f + 1);
23
+ }, Math.max(16, Math.round(1000 / fps)));
24
+ return () => clearInterval(id);
25
+ }, [runtime, fps, paused]);
26
+ const canvas = new Canvas(def.width, def.height);
27
+ runtime.draw(canvas);
28
+ const rows = canvas.rows();
29
+ return (_jsxs(Box, { flexDirection: "column", width: def.width, children: [rows.map((row, y) => (_jsx(Text, { wrap: "truncate", children: row.map((seg, i) => seg.tone ? (_jsx(Text, { color: colors[seg.tone], children: seg.text }, i)) : (seg.text)) }, y))), showStatus && (_jsx(Text, { dimColor: true, wrap: "truncate", children: statusText ?? runtime.status() }))] }));
30
+ });
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useMemo } from 'react';
3
+ import { AgentScene } from './index.js';
4
+ import { palette } from '../theme/theme.js';
5
+ /** The bundled scenes, recolored with SOJA's active light or dark palette. */
6
+ export function Mascot({ scene, paused = false }) {
7
+ const colors = useMemo(() => ({
8
+ agent: palette.accent,
9
+ rock: palette.muted,
10
+ soil: palette.faint,
11
+ dirt: palette.warning,
12
+ spark: palette.warning,
13
+ fire: palette.danger,
14
+ flame: palette.warning,
15
+ water: palette.info,
16
+ good: palette.success,
17
+ bad: palette.danger,
18
+ accent: palette.accent,
19
+ cloud: palette.muted,
20
+ wood: palette.warning,
21
+ paper: palette.text,
22
+ leaf: palette.success,
23
+ text: palette.text,
24
+ }), []);
25
+ return _jsx(AgentScene, { scene: scene, palette: colors, fps: 4, paused: paused, showStatus: false });
26
+ }
@@ -0,0 +1,25 @@
1
+ /* eslint-disable @typescript-eslint/no-non-null-assertion -- ANSI frame encoding indexes rows created from canvas dimensions. */
2
+ import { Canvas } from './engine.js';
3
+ import { defaultPalette } from './palette.js';
4
+ function hexToAnsi(hex) {
5
+ const m = /^#?([0-9a-f]{6})$/i.exec(hex);
6
+ if (!m)
7
+ return '';
8
+ const n = parseInt(m[1], 16);
9
+ return `\x1b[38;2;${(n >> 16) & 255};${(n >> 8) & 255};${n & 255}m`;
10
+ }
11
+ /** Renderiza un fotograma como texto ANSI (útil fuera de Ink: logs, CLI simple, tests). */
12
+ export function frameToAnsi(def, runtime, palette = defaultPalette) {
13
+ const c = new Canvas(def.width, def.height);
14
+ runtime.draw(c);
15
+ return c
16
+ .rows()
17
+ .map((row) => row.map((s) => (s.tone ? hexToAnsi(palette[s.tone]) + s.text + '\x1b[0m' : s.text)).join(''))
18
+ .join('\n');
19
+ }
20
+ /** Renderiza un fotograma como texto plano. */
21
+ export function frameToText(def, runtime) {
22
+ const c = new Canvas(def.width, def.height);
23
+ runtime.draw(c);
24
+ return c.toString();
25
+ }