ubume 0.1.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 (182) hide show
  1. package/README.md +88 -0
  2. package/bin/codexa-local-harness-bridge.js +3 -0
  3. package/bin/codexa.js +28 -0
  4. package/bin/ubume-local-harness-bridge.js +325 -0
  5. package/bin/ubume.js +398 -0
  6. package/package.json +66 -0
  7. package/src/app.tsx +5759 -0
  8. package/src/commands/handler.ts +889 -0
  9. package/src/config/appVersion.ts +69 -0
  10. package/src/config/buildInfo.ts +3 -0
  11. package/src/config/launchArgs.ts +196 -0
  12. package/src/config/layeredConfig.ts +853 -0
  13. package/src/config/legacyEnv.ts +16 -0
  14. package/src/config/persistence.ts +377 -0
  15. package/src/config/runtimeConfig.ts +558 -0
  16. package/src/config/settings.ts +405 -0
  17. package/src/config/toml-serialize.ts +109 -0
  18. package/src/config/trustStore.ts +84 -0
  19. package/src/config/updateCheckCache.ts +85 -0
  20. package/src/core/README.md +55 -0
  21. package/src/core/agent/loop.ts +464 -0
  22. package/src/core/agent/protocol.ts +345 -0
  23. package/src/core/agent/tools.ts +423 -0
  24. package/src/core/auth/codexAuth.ts +359 -0
  25. package/src/core/codex/codexExecArgs.ts +166 -0
  26. package/src/core/codex/codexLaunch.ts +163 -0
  27. package/src/core/codex/codexPrompt.ts +429 -0
  28. package/src/core/debug/inputDebug.ts +51 -0
  29. package/src/core/debug/localStreamDebug.ts +50 -0
  30. package/src/core/debug/modelStateDebug.ts +35 -0
  31. package/src/core/executables/antigravityExecutable.ts +48 -0
  32. package/src/core/executables/claudeExecutable.ts +63 -0
  33. package/src/core/executables/codexExecutable.ts +160 -0
  34. package/src/core/executables/executableResolver.ts +164 -0
  35. package/src/core/executables/geminiExecutable.ts +78 -0
  36. package/src/core/models/codexCapabilities.ts +97 -0
  37. package/src/core/models/codexModelCapabilities.ts +624 -0
  38. package/src/core/models/codexModelsCacheSeed.ts +153 -0
  39. package/src/core/models/modelSpecs.ts +303 -0
  40. package/src/core/models/providerModelCache.ts +94 -0
  41. package/src/core/perf/profiler.ts +125 -0
  42. package/src/core/perf/renderDebug.ts +398 -0
  43. package/src/core/process/CommandRunner.ts +280 -0
  44. package/src/core/process/processValidation.ts +111 -0
  45. package/src/core/providerLauncher/launcher.ts +220 -0
  46. package/src/core/providerLauncher/registry.ts +354 -0
  47. package/src/core/providerLauncher/types.ts +95 -0
  48. package/src/core/providerLauncher/workspaceConfig.ts +487 -0
  49. package/src/core/providerRuntime/anthropic.ts +580 -0
  50. package/src/core/providerRuntime/antigravity.ts +500 -0
  51. package/src/core/providerRuntime/capabilityProfile.ts +383 -0
  52. package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -0
  53. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  54. package/src/core/providerRuntime/codexaCupy.ts +97 -0
  55. package/src/core/providerRuntime/codexaNative.ts +425 -0
  56. package/src/core/providerRuntime/contextMetadata.ts +397 -0
  57. package/src/core/providerRuntime/gemini.ts +789 -0
  58. package/src/core/providerRuntime/lmstudio.ts +118 -0
  59. package/src/core/providerRuntime/local.ts +770 -0
  60. package/src/core/providerRuntime/localHarness/runtime.ts +1090 -0
  61. package/src/core/providerRuntime/localOutputBudget.ts +17 -0
  62. package/src/core/providerRuntime/mistralVibe.ts +667 -0
  63. package/src/core/providerRuntime/models.ts +175 -0
  64. package/src/core/providerRuntime/reasoning.ts +20 -0
  65. package/src/core/providerRuntime/registry.ts +286 -0
  66. package/src/core/providerRuntime/types.ts +142 -0
  67. package/src/core/providerRuntime/unsloth.ts +216 -0
  68. package/src/core/providers/codexJsonStream.ts +305 -0
  69. package/src/core/providers/codexSubprocess.ts +378 -0
  70. package/src/core/providers/codexTranscript.ts +695 -0
  71. package/src/core/providers/openaiNative.ts +13 -0
  72. package/src/core/providers/registry.ts +21 -0
  73. package/src/core/providers/types.ts +91 -0
  74. package/src/core/shared/attachments.ts +101 -0
  75. package/src/core/shared/cleanupFastFail.ts +67 -0
  76. package/src/core/shared/clipboard.ts +24 -0
  77. package/src/core/shared/clipboardImage.ts +111 -0
  78. package/src/core/shared/githubDiagnostics.ts +222 -0
  79. package/src/core/shared/hollowResponseFormat.ts +39 -0
  80. package/src/core/terminal/clearFrameBoundary.ts +852 -0
  81. package/src/core/terminal/frameLock.ts +110 -0
  82. package/src/core/terminal/inkRenderReset.ts +123 -0
  83. package/src/core/terminal/startupClear.ts +20 -0
  84. package/src/core/terminal/terminalCapabilities.ts +100 -0
  85. package/src/core/terminal/terminalControl.ts +169 -0
  86. package/src/core/terminal/terminalSanitize.ts +147 -0
  87. package/src/core/terminal/terminalTitle.ts +400 -0
  88. package/src/core/version/channel.ts +27 -0
  89. package/src/core/version/packageManager.ts +119 -0
  90. package/src/core/version/updateCheck.ts +203 -0
  91. package/src/core/workspace/appData.ts +107 -0
  92. package/src/core/workspace/conversationStore.ts +335 -0
  93. package/src/core/workspace/launchContext.ts +259 -0
  94. package/src/core/workspace/planStorage.ts +135 -0
  95. package/src/core/workspace/projectInstructions.ts +54 -0
  96. package/src/core/workspace/scratchDir.ts +64 -0
  97. package/src/core/workspace/workspaceActivity.ts +384 -0
  98. package/src/core/workspace/workspaceGuard.ts +377 -0
  99. package/src/core/workspace/workspaceRoot.ts +47 -0
  100. package/src/exec.ts +73 -0
  101. package/src/headless/execArgs.ts +296 -0
  102. package/src/headless/execRunner.ts +304 -0
  103. package/src/index.tsx +270 -0
  104. package/src/legacyEnvBootstrap.ts +5 -0
  105. package/src/session/appSession.ts +771 -0
  106. package/src/session/chatLifecycle.ts +994 -0
  107. package/src/session/conversation.ts +107 -0
  108. package/src/session/liveRenderScheduler.ts +214 -0
  109. package/src/session/persistedResponse.ts +93 -0
  110. package/src/session/planFlow.ts +159 -0
  111. package/src/session/planTranscript.ts +19 -0
  112. package/src/session/promptRunSchedule.ts +26 -0
  113. package/src/session/types.ts +234 -0
  114. package/src/test/runtimeTestUtils.ts +14 -0
  115. package/src/types/react-dom.d.ts +3 -0
  116. package/src/ui/chrome/ActivityBars.tsx +68 -0
  117. package/src/ui/chrome/ActivityIndicator.tsx +58 -0
  118. package/src/ui/chrome/AnimatedStatusText.tsx +69 -0
  119. package/src/ui/chrome/AppShell.tsx +474 -0
  120. package/src/ui/chrome/BottomComposer.tsx +1135 -0
  121. package/src/ui/chrome/DashCard.tsx +82 -0
  122. package/src/ui/chrome/RunFooter.tsx +65 -0
  123. package/src/ui/chrome/RuntimeStatusBar.tsx +108 -0
  124. package/src/ui/chrome/Spinner.tsx +25 -0
  125. package/src/ui/chrome/TopHeader.tsx +439 -0
  126. package/src/ui/chrome/UpdateAvailableCard.tsx +42 -0
  127. package/src/ui/chrome/busyStatusAnimation.ts +11 -0
  128. package/src/ui/input/commandNormalize.ts +66 -0
  129. package/src/ui/input/focus.ts +73 -0
  130. package/src/ui/input/imageAttachments.ts +18 -0
  131. package/src/ui/input/inputBuffer.ts +203 -0
  132. package/src/ui/input/pastedContent.ts +75 -0
  133. package/src/ui/input/rawArrowKeys.ts +28 -0
  134. package/src/ui/input/slashCommands.ts +43 -0
  135. package/src/ui/input/useStdinRawModeLease.ts +23 -0
  136. package/src/ui/layout.ts +560 -0
  137. package/src/ui/panels/AttachmentImportPanel.tsx +131 -0
  138. package/src/ui/panels/AuthPanel.tsx +149 -0
  139. package/src/ui/panels/BackendPicker.tsx +28 -0
  140. package/src/ui/panels/ModePicker.tsx +31 -0
  141. package/src/ui/panels/ModelPicker.tsx +31 -0
  142. package/src/ui/panels/ModelPickerScreen.tsx +761 -0
  143. package/src/ui/panels/ModelReasoningPicker.tsx +458 -0
  144. package/src/ui/panels/Panel.tsx +51 -0
  145. package/src/ui/panels/PermissionsPanel.tsx +78 -0
  146. package/src/ui/panels/PlanActionPicker.tsx +187 -0
  147. package/src/ui/panels/ProviderPicker.tsx +753 -0
  148. package/src/ui/panels/ProviderSetupPrompt.tsx +52 -0
  149. package/src/ui/panels/ReasoningPicker.tsx +46 -0
  150. package/src/ui/panels/ResumePicker.tsx +90 -0
  151. package/src/ui/panels/SelectionPanel.tsx +138 -0
  152. package/src/ui/panels/SettingsPanel.tsx +156 -0
  153. package/src/ui/panels/TextEntryPanel.tsx +139 -0
  154. package/src/ui/panels/ThemePicker.tsx +32 -0
  155. package/src/ui/panels/ToolApprovalPanel.tsx +46 -0
  156. package/src/ui/panels/UpdatePromptPanel.tsx +236 -0
  157. package/src/ui/panels/responsivePickerViewport.ts +64 -0
  158. package/src/ui/render/Markdown.tsx +331 -0
  159. package/src/ui/render/diffRenderer.ts +116 -0
  160. package/src/ui/render/logoVariants.ts +113 -0
  161. package/src/ui/render/modeDisplay.ts +52 -0
  162. package/src/ui/render/outputPipeline.ts +64 -0
  163. package/src/ui/render/runtimeDisplay.ts +128 -0
  164. package/src/ui/render/terminalAnswerFormat.ts +128 -0
  165. package/src/ui/render/textLayout.ts +392 -0
  166. package/src/ui/theme.tsx +274 -0
  167. package/src/ui/themeFlow.ts +41 -0
  168. package/src/ui/timeline/ActionRequiredBlock.tsx +38 -0
  169. package/src/ui/timeline/AgentBlock.tsx +130 -0
  170. package/src/ui/timeline/StaticIntroItem.tsx +54 -0
  171. package/src/ui/timeline/ThinkingBlock.tsx +100 -0
  172. package/src/ui/timeline/Timeline.tsx +1410 -0
  173. package/src/ui/timeline/TranscriptShell.tsx +302 -0
  174. package/src/ui/timeline/TurnGroup.tsx +673 -0
  175. package/src/ui/timeline/layoutListWindow.ts +145 -0
  176. package/src/ui/timeline/liveViewportWindow.ts +68 -0
  177. package/src/ui/timeline/progressEntries.ts +156 -0
  178. package/src/ui/timeline/runActivityView.ts +37 -0
  179. package/src/ui/timeline/staticTranscriptCache.ts +174 -0
  180. package/src/ui/timeline/streamCoalesce.ts +53 -0
  181. package/src/ui/timeline/timelineMeasure.ts +3273 -0
  182. package/src/ui/useThrottledValue.ts +31 -0
@@ -0,0 +1,234 @@
1
+ import type {
2
+ AuthPreference,
3
+ AvailableBackend,
4
+ } from "../config/settings.js";
5
+ import type { ResolvedRuntimeConfig } from "../config/runtimeConfig.js";
6
+ import type { RunActivitySummary, RunFileActivity } from "../core/workspace/workspaceActivity.js";
7
+
8
+ // ─── Screen routing ──────────────────────────────────────────────────────────
9
+
10
+ export type Screen =
11
+ | "main"
12
+ | "resume-picker"
13
+ | "model-picker"
14
+ | "mode-picker"
15
+ | "backend-picker"
16
+ | "provider-picker"
17
+ | "provider-setup"
18
+ | "auth-panel"
19
+ | "reasoning-picker"
20
+ | "theme-picker"
21
+ | "settings-panel"
22
+ | "permissions-panel"
23
+ | "permissions-approval-picker"
24
+ | "permissions-sandbox-picker"
25
+ | "permissions-network-picker"
26
+ | "permissions-add-writable-root"
27
+ | "permissions-remove-writable-root"
28
+ | "import-confirmation"
29
+ | "tool-approval"
30
+ | "update-prompt";
31
+
32
+ // ─── Provider readiness ───────────────────────────────────────────────────────
33
+
34
+ export type ExternalCliStatus = "idle" | "starting" | "ready" | "failed";
35
+
36
+ // ─── UI State Machine ─────────────────────────────────────────────────────────
37
+ // Drives all visual decisions: border colors, input persona, turn opacity.
38
+ //
39
+ // IDLE ──submit──▶ THINKING ──first token──▶ RESPONDING ──answer visible──▶ ANSWER_VISIBLE ──complete──▶ IDLE
40
+ // ▲ │ │ │
41
+ // │ └──error──▶ ERROR └──question──▶ AWAITING
42
+ // └──────────────────────────────────────── dismiss / answer ──────────┘
43
+
44
+ export type UIState =
45
+ | { kind: "IDLE" }
46
+ | { kind: "THINKING"; turnId: number }
47
+ | { kind: "RESPONDING"; turnId: number }
48
+ | { kind: "ANSWER_VISIBLE"; turnId: number }
49
+ | { kind: "AWAITING_USER_ACTION"; turnId: number; question: string }
50
+ | { kind: "ERROR"; turnId: number; message: string }
51
+ | { kind: "SHELL_RUNNING"; shellId: number };
52
+
53
+ /** Derive the legacy busy flag from UIState for guard functions. */
54
+ export function isBusy(state: UIState): boolean {
55
+ return state.kind === "THINKING"
56
+ || state.kind === "RESPONDING"
57
+ || state.kind === "ANSWER_VISIBLE"
58
+ || state.kind === "SHELL_RUNNING";
59
+ }
60
+
61
+ // ─── Timeline events ─────────────────────────────────────────────────────────
62
+
63
+ export interface TimelineBaseEvent {
64
+ id: number;
65
+ createdAt: number;
66
+ }
67
+
68
+ export interface UserPromptEvent extends TimelineBaseEvent {
69
+ type: "user";
70
+ prompt: string;
71
+ /** Links this user prompt to its run and assistant response in a TurnGroup. */
72
+ turnId: number;
73
+ }
74
+
75
+ export interface AssistantEvent extends TimelineBaseEvent {
76
+ type: "assistant";
77
+ content: string;
78
+ /** Accumulated chunks during streaming — avoids O(n²) string concatenation. */
79
+ contentChunks: string[];
80
+ /** Links this response back to the originating user prompt + run. */
81
+ turnId: number;
82
+ }
83
+
84
+ /**
85
+ * Returns the full assistant content string. During streaming, joins the
86
+ * accumulated chunks (single O(n) allocation). After finalization, returns
87
+ * the pre-joined `content` field directly.
88
+ */
89
+ export function getAssistantContent(event: AssistantEvent | null | undefined): string {
90
+ if (!event) return "";
91
+ if (event.contentChunks.length > 0) return event.contentChunks.join("");
92
+ return event.content;
93
+ }
94
+
95
+ export function getResponseSegmentText(segment: RunResponseSegment | null | undefined): string {
96
+ if (!segment) return "";
97
+ return segment.chunks.join("");
98
+ }
99
+
100
+ export interface RunToolActivity {
101
+ id: string;
102
+ command: string;
103
+ status: "running" | "completed" | "failed";
104
+ startedAt: number;
105
+ completedAt?: number | null;
106
+ summary?: string | null;
107
+ /** Turn-global ordering against thinking blocks and response segments. */
108
+ streamSeq?: number;
109
+ }
110
+
111
+ export type RunProgressSource =
112
+ | "reasoning"
113
+ | "todo"
114
+ | "stdout"
115
+ | "tool"
116
+ | "activity"
117
+ | "stderr"
118
+ | "transcript";
119
+
120
+ export interface RunProgressBlock {
121
+ id: string;
122
+ text: string;
123
+ sequence: number;
124
+ createdAt: number;
125
+ updatedAt: number;
126
+ status: "active" | "completed";
127
+ /** Turn-global ordering against tools and response segments. Set when block first becomes visible. */
128
+ streamSeq?: number;
129
+ }
130
+
131
+ export interface RunResponseSegment {
132
+ id: string;
133
+ streamSeq: number;
134
+ chunks: string[];
135
+ status: "active" | "completed";
136
+ startedAt: number;
137
+ }
138
+
139
+ export interface RunPlanBlock {
140
+ id: string;
141
+ streamSeq: number;
142
+ chunks: string[];
143
+ status: "active" | "completed";
144
+ startedAt: number;
145
+ }
146
+
147
+ export function getRunPlanText(plan: RunPlanBlock | null | undefined): string {
148
+ if (!plan) return "";
149
+ return plan.chunks.join("");
150
+ }
151
+
152
+ export interface RunStreamItem {
153
+ streamSeq: number;
154
+ kind: "thinking" | "action" | "response" | "plan";
155
+ /** block.id, tool.id, response-segment id, or plan.id depending on kind. */
156
+ refId: string;
157
+ }
158
+
159
+ export interface RunProgressEntry {
160
+ id: string;
161
+ source: RunProgressSource;
162
+ /** Latest normalized full text received for this upstream progress item. */
163
+ text: string;
164
+ sequence: number;
165
+ createdAt: number;
166
+ updatedAt: number;
167
+ /** Structured visible blocks derived from `text` with stable identities. */
168
+ blocks: RunProgressBlock[];
169
+ /**
170
+ * Count of trailing newlines that have not yet been committed into a block.
171
+ * This keeps single-newline vs double-newline boundaries streaming-safe.
172
+ */
173
+ pendingNewlineCount: number;
174
+ }
175
+
176
+ export interface SystemEvent extends TimelineBaseEvent {
177
+ type: "system";
178
+ title: string;
179
+ content: string;
180
+ }
181
+
182
+ export interface ErrorEvent extends TimelineBaseEvent {
183
+ type: "error";
184
+ title: string;
185
+ content: string;
186
+ }
187
+
188
+ export interface RunEvent extends TimelineBaseEvent {
189
+ type: "run";
190
+ backendId: AvailableBackend;
191
+ backendLabel: string;
192
+ runtime: ResolvedRuntimeConfig;
193
+ prompt: string;
194
+ progressEntries: RunProgressEntry[];
195
+ status: "running" | "completed" | "failed" | "canceled";
196
+ summary: string;
197
+ truncatedOutput: boolean;
198
+ startedAt: number;
199
+ durationMs: number | null;
200
+ toolActivities: RunToolActivity[];
201
+ activity: RunFileActivity[];
202
+ touchedFileCount: number;
203
+ activitySummary?: RunActivitySummary;
204
+ errorMessage?: string | null;
205
+ /** Links this run to its user prompt for TurnGroup rendering. */
206
+ turnId: number;
207
+ /** Chronologically ordered references to thinking/action/response items. */
208
+ streamItems?: RunStreamItem[];
209
+ /** Discrete response segments — interrupted by thinking/action events. */
210
+ responseSegments?: RunResponseSegment[];
211
+ /** Monotonic counter the reducer uses to assign streamSeq across all events. */
212
+ lastStreamSeq?: number;
213
+ /** When set, the next assistant delta extends this segment; cleared by thinking/action. */
214
+ activeResponseSegmentId?: string | null;
215
+ /** First-class plan content rendered as a stable stream item. */
216
+ plan?: RunPlanBlock | null;
217
+ /** The approved plan text, injected at the start of execution turns. */
218
+ approvedPlan?: string;
219
+ /** How the final assistant text of this run is presented. Set at run creation. */
220
+ responsePresentation?: "assistant" | "plan";
221
+ }
222
+
223
+ export interface ShellEvent extends TimelineBaseEvent {
224
+ type: "shell";
225
+ command: string;
226
+ lines: string[];
227
+ stderrLines: string[];
228
+ summary?: string | null;
229
+ status: "running" | "completed" | "failed";
230
+ exitCode: number | null;
231
+ durationMs: number | null;
232
+ }
233
+
234
+ export type TimelineEvent = UserPromptEvent | AssistantEvent | SystemEvent | ErrorEvent | RunEvent | ShellEvent;
@@ -0,0 +1,14 @@
1
+ import { resolveRuntimeConfig, type RuntimeConfig, DEFAULT_RUNTIME_CONFIG, type ResolvedRuntimeConfig } from "../config/runtimeConfig.js";
2
+
3
+ export function makeResolvedRuntime(overrides: Partial<RuntimeConfig> = {}): ResolvedRuntimeConfig {
4
+ return resolveRuntimeConfig({
5
+ ...DEFAULT_RUNTIME_CONFIG,
6
+ ...overrides,
7
+ policy: {
8
+ ...DEFAULT_RUNTIME_CONFIG.policy,
9
+ ...overrides.policy,
10
+ },
11
+ });
12
+ }
13
+
14
+ export const TEST_RUNTIME = makeResolvedRuntime();
@@ -0,0 +1,3 @@
1
+ declare module "react-dom" {
2
+ export function flushSync<T>(callback: () => T): T;
3
+ }
@@ -0,0 +1,68 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import { useTheme } from "../theme.js";
4
+
5
+ // ─── WaveBar ─────────────────────────────────────────────────────────────────
6
+ // Audio-visualizer style wave. Renders as a single <Text> node to minimise
7
+ // the React element tree — important for keeping Ink re-renders cheap.
8
+
9
+ const WAVE_CHARS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"] as const;
10
+ const WAVE_COLS = 12;
11
+ const WAVE_SPEED = 0.38; // radians per tick
12
+ const WAVE_SPREAD = 0.6; // phase offset between adjacent columns
13
+
14
+ interface WaveBarProps {
15
+ tick: number;
16
+ color?: string;
17
+ }
18
+
19
+ export function WaveBar({ tick, color }: WaveBarProps) {
20
+ const theme = useTheme();
21
+ const activeColor = color ?? theme.accent;
22
+
23
+ let str = "";
24
+ for (let c = 0; c < WAVE_COLS; c++) {
25
+ const phase = tick * WAVE_SPEED + c * WAVE_SPREAD;
26
+ const normalized = (Math.sin(phase) + 1) / 2; // [0, 1]
27
+ const idx = Math.round(normalized * (WAVE_CHARS.length - 1));
28
+ str += WAVE_CHARS[idx] ?? "▄";
29
+ }
30
+
31
+ return <Text color={activeColor}>{str}</Text>;
32
+ }
33
+
34
+ // ─── PulseBar ─────────────────────────────────────────────────────────────────
35
+ // Indeterminate progress bar. Renders as 3 <Text> nodes (before / fill / after).
36
+
37
+ const PULSE_TOTAL = 20;
38
+ const PULSE_FILL = 6;
39
+ const PULSE_MAX_POS = PULSE_TOTAL - PULSE_FILL; // 14
40
+ const PULSE_PERIOD = PULSE_MAX_POS * 2; // 28
41
+ const PULSE_SPEED = 2; // ticks per position step
42
+
43
+ interface PulseBarProps {
44
+ tick: number;
45
+ color?: string;
46
+ dimColor?: string;
47
+ }
48
+
49
+ export function PulseBar({ tick, color, dimColor }: PulseBarProps) {
50
+ const theme = useTheme();
51
+ const activeColor = color ?? theme.accent;
52
+ const inactiveColor = dimColor ?? theme.textDim;
53
+
54
+ const raw = Math.floor(tick / PULSE_SPEED) % PULSE_PERIOD;
55
+ const pos = raw < PULSE_MAX_POS ? raw : PULSE_PERIOD - raw;
56
+
57
+ const before = "░".repeat(pos);
58
+ const fill = "█".repeat(PULSE_FILL);
59
+ const after = "░".repeat(PULSE_TOTAL - pos - PULSE_FILL);
60
+
61
+ return (
62
+ <Box>
63
+ <Text color={inactiveColor}>{before}</Text>
64
+ <Text color={activeColor}>{fill}</Text>
65
+ <Text color={inactiveColor}>{after}</Text>
66
+ </Box>
67
+ );
68
+ }
@@ -0,0 +1,58 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { Text } from "ink";
3
+ import type { UIState, ExternalCliStatus } from "../../session/types.js";
4
+ import { useTheme } from "../theme.js";
5
+
6
+ const SPINNER_FRAMES = ["?", "?", "?", "?", "?", "?", "?", "?", "?", "?"];
7
+ const STREAMING_FRAMES = ["?", "?", "?", "?"];
8
+
9
+ export interface ActivityIndicatorProps {
10
+ uiState: UIState;
11
+ externalCliStatus?: ExternalCliStatus;
12
+ }
13
+
14
+ export function ActivityIndicator({ uiState, externalCliStatus = "idle" }: ActivityIndicatorProps) {
15
+ const theme = useTheme();
16
+ const [frameIndex, setFrameIndex] = useState(0);
17
+
18
+ const isError = uiState.kind === "ERROR" || externalCliStatus === "failed";
19
+ const isStarting = externalCliStatus === "starting";
20
+ const isThinking = uiState.kind === "THINKING";
21
+ const isStreaming = uiState.kind === "RESPONDING";
22
+ const isAction = uiState.kind === "SHELL_RUNNING";
23
+
24
+ const isAnimated = isStarting || isThinking || isStreaming;
25
+
26
+ useEffect(() => {
27
+ if (!isAnimated) return;
28
+ const interval = setInterval(() => {
29
+ setFrameIndex((prev) => prev + 1);
30
+ }, 80);
31
+ return () => clearInterval(interval);
32
+ }, [isAnimated]);
33
+
34
+ let glyph = "?";
35
+ let color = theme.textDim;
36
+ let bold = false;
37
+
38
+ if (isError) {
39
+ glyph = "�";
40
+ color = theme.error;
41
+ bold = true;
42
+ } else if (isAction) {
43
+ glyph = "?";
44
+ color = theme.warning;
45
+ bold = true;
46
+ } else if (isStarting || isThinking) {
47
+ glyph = SPINNER_FRAMES[frameIndex % SPINNER_FRAMES.length]!;
48
+ color = theme.text;
49
+ } else if (isStreaming) {
50
+ glyph = STREAMING_FRAMES[frameIndex % STREAMING_FRAMES.length]!;
51
+ color = theme.success;
52
+ } else {
53
+ glyph = "?";
54
+ color = theme.textDim;
55
+ }
56
+
57
+ return <Text color={color} bold={bold}>{glyph}</Text>;
58
+ }
@@ -0,0 +1,69 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { Text } from "ink";
3
+ import * as renderDebug from "../../core/perf/renderDebug.js";
4
+ import { useTheme } from "../theme.js";
5
+ import { sanitizeTerminalOutput } from "../../core/terminal/terminalSanitize.js";
6
+ import { BUSY_STATUS_FRAME_MS, BUSY_STATUS_FRAMES, getBusyStatusFrame } from "./busyStatusAnimation.js";
7
+
8
+ interface AnimatedStatusTextProps {
9
+ baseText: string;
10
+ isActive: boolean;
11
+ isError?: boolean;
12
+ animationFrame?: string;
13
+ }
14
+
15
+ function useLocalBusyStatusFrame(isActive: boolean, label: string): string {
16
+ const [frameIndex, setFrameIndex] = useState(0);
17
+ const staticStatus = process.env.UBUME_DEBUG_STATIC_STATUS === "1";
18
+
19
+ useEffect(() => {
20
+ if (!isActive || staticStatus) {
21
+ setFrameIndex(0);
22
+ return;
23
+ }
24
+
25
+ setFrameIndex(0);
26
+ const timer = setInterval(() => {
27
+ setFrameIndex((current) => {
28
+ const next = current + 1;
29
+ renderDebug.traceStatusTick({ owner: "Status", label, frameIndex: next });
30
+ return next;
31
+ });
32
+ }, BUSY_STATUS_FRAME_MS);
33
+ timer.unref?.();
34
+
35
+ return () => {
36
+ clearInterval(timer);
37
+ };
38
+ }, [isActive, label, staticStatus]);
39
+
40
+ if (isActive && staticStatus) {
41
+ return BUSY_STATUS_FRAMES[BUSY_STATUS_FRAMES.length - 1]!;
42
+ }
43
+ return getBusyStatusFrame(frameIndex);
44
+ }
45
+
46
+ export function AnimatedStatusText({ baseText, isActive, isError = false, animationFrame }: AnimatedStatusTextProps) {
47
+ const localFrame = useLocalBusyStatusFrame(isActive, baseText);
48
+ renderDebug.useRenderDebug("Status", {
49
+ baseText,
50
+ isActive,
51
+ isError,
52
+ animationFrame: animationFrame ?? localFrame,
53
+ });
54
+ renderDebug.useLifecycleDebug("Status", {
55
+ baseText,
56
+ isActive,
57
+ isError,
58
+ });
59
+
60
+ const theme = useTheme();
61
+ const renderedText = sanitizeTerminalOutput(baseText);
62
+ const suffix = isActive ? animationFrame ?? localFrame : "";
63
+
64
+ return (
65
+ <Text color={isError ? theme.error : theme.info} wrap="truncate">
66
+ {renderedText}{suffix}
67
+ </Text>
68
+ );
69
+ }