@kenkaiiii/gg-boss 4.3.140 → 4.3.141

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 (162) hide show
  1. package/dist/{chunk-JVQDTPYR.js → chunk-3EWLK53W.js} +18 -9
  2. package/dist/{chunk-JVQDTPYR.js.map → chunk-3EWLK53W.js.map} +1 -1
  3. package/dist/{chunk-5WNYQQPQ.js → chunk-QT366Y52.js} +4 -3
  4. package/dist/{chunk-5WNYQQPQ.js.map → chunk-QT366Y52.js.map} +1 -1
  5. package/dist/{chunk-PQAHDHVY.js → chunk-WJ4S4TOY.js} +3 -2
  6. package/dist/{chunk-PQAHDHVY.js.map → chunk-WJ4S4TOY.js.map} +1 -1
  7. package/dist/{chunk-B2WQ5E5J.js → chunk-YNWFCUMR.js} +2 -1
  8. package/dist/{chunk-B2WQ5E5J.js.map → chunk-YNWFCUMR.js.map} +1 -1
  9. package/dist/cli.js +2246 -182
  10. package/dist/cli.js.map +1 -1
  11. package/dist/{devtools-VBUDNGEI.js → devtools-4TI4D7F2.js} +3 -2
  12. package/dist/{devtools-VBUDNGEI.js.map → devtools-4TI4D7F2.js.map} +1 -1
  13. package/dist/{dist-7DAPKZGX.js → dist-VXOVSHZ5.js} +3 -2
  14. package/dist/{dist-7DAPKZGX.js.map → dist-VXOVSHZ5.js.map} +1 -1
  15. package/dist/{ignore-3AEIALHQ.js → ignore-76P4EAAU.js} +3 -2
  16. package/dist/{ignore-3AEIALHQ.js.map → ignore-76P4EAAU.js.map} +1 -1
  17. package/dist/index.js +21 -4
  18. package/dist/index.js.map +1 -1
  19. package/dist/{out-D65DTPFZ.js → out-XEXARMKS.js} +3 -2
  20. package/dist/{out-D65DTPFZ.js.map → out-XEXARMKS.js.map} +1 -1
  21. package/dist/pixel-WPYTQADG.js +14 -0
  22. package/dist/{pixel-fix-ALWXCLTS.js → pixel-fix-4WGZAJ5W.js} +4 -3
  23. package/dist/{pixel-fix-ALWXCLTS.js.map → pixel-fix-4WGZAJ5W.js.map} +1 -1
  24. package/package.json +10 -11
  25. package/dist/audio.d.ts +0 -21
  26. package/dist/audio.d.ts.map +0 -1
  27. package/dist/audio.js +0 -231
  28. package/dist/audio.js.map +0 -1
  29. package/dist/audio.test.d.ts +0 -2
  30. package/dist/audio.test.d.ts.map +0 -1
  31. package/dist/audio.test.js +0 -13
  32. package/dist/audio.test.js.map +0 -1
  33. package/dist/auto-update.d.ts +0 -24
  34. package/dist/auto-update.d.ts.map +0 -1
  35. package/dist/auto-update.js +0 -231
  36. package/dist/auto-update.js.map +0 -1
  37. package/dist/banner.d.ts +0 -17
  38. package/dist/banner.d.ts.map +0 -1
  39. package/dist/banner.js +0 -25
  40. package/dist/banner.js.map +0 -1
  41. package/dist/boss-footer.d.ts +0 -25
  42. package/dist/boss-footer.d.ts.map +0 -1
  43. package/dist/boss-footer.js +0 -107
  44. package/dist/boss-footer.js.map +0 -1
  45. package/dist/boss-phrases.d.ts +0 -9
  46. package/dist/boss-phrases.d.ts.map +0 -1
  47. package/dist/boss-phrases.js +0 -71
  48. package/dist/boss-phrases.js.map +0 -1
  49. package/dist/boss-store.d.ts +0 -245
  50. package/dist/boss-store.d.ts.map +0 -1
  51. package/dist/boss-store.js +0 -623
  52. package/dist/boss-store.js.map +0 -1
  53. package/dist/boss-system-prompt.d.ts +0 -3
  54. package/dist/boss-system-prompt.d.ts.map +0 -1
  55. package/dist/boss-system-prompt.js +0 -180
  56. package/dist/boss-system-prompt.js.map +0 -1
  57. package/dist/boss-tasks-overlay.d.ts +0 -22
  58. package/dist/boss-tasks-overlay.d.ts.map +0 -1
  59. package/dist/boss-tasks-overlay.js +0 -157
  60. package/dist/boss-tasks-overlay.js.map +0 -1
  61. package/dist/branding.d.ts +0 -32
  62. package/dist/branding.d.ts.map +0 -1
  63. package/dist/branding.js +0 -59
  64. package/dist/branding.js.map +0 -1
  65. package/dist/cli.d.ts +0 -3
  66. package/dist/cli.d.ts.map +0 -1
  67. package/dist/cli.smoke.test.d.ts +0 -2
  68. package/dist/cli.smoke.test.d.ts.map +0 -1
  69. package/dist/cli.smoke.test.js +0 -48
  70. package/dist/cli.smoke.test.js.map +0 -1
  71. package/dist/colors.d.ts +0 -14
  72. package/dist/colors.d.ts.map +0 -1
  73. package/dist/colors.js +0 -31
  74. package/dist/colors.js.map +0 -1
  75. package/dist/discover.d.ts +0 -13
  76. package/dist/discover.d.ts.map +0 -1
  77. package/dist/discover.js +0 -92
  78. package/dist/discover.js.map +0 -1
  79. package/dist/event-queue.d.ts +0 -16
  80. package/dist/event-queue.d.ts.map +0 -1
  81. package/dist/event-queue.js +0 -39
  82. package/dist/event-queue.js.map +0 -1
  83. package/dist/index.d.ts +0 -6
  84. package/dist/index.d.ts.map +0 -1
  85. package/dist/link-command.d.ts +0 -2
  86. package/dist/link-command.d.ts.map +0 -1
  87. package/dist/link-command.js +0 -120
  88. package/dist/link-command.js.map +0 -1
  89. package/dist/links.d.ts +0 -11
  90. package/dist/links.d.ts.map +0 -1
  91. package/dist/links.js +0 -22
  92. package/dist/links.js.map +0 -1
  93. package/dist/logger.d.ts +0 -41
  94. package/dist/logger.d.ts.map +0 -1
  95. package/dist/logger.js +0 -112
  96. package/dist/logger.js.map +0 -1
  97. package/dist/orchestrator-app.d.ts +0 -15
  98. package/dist/orchestrator-app.d.ts.map +0 -1
  99. package/dist/orchestrator-app.js +0 -599
  100. package/dist/orchestrator-app.js.map +0 -1
  101. package/dist/orchestrator.d.ts +0 -147
  102. package/dist/orchestrator.d.ts.map +0 -1
  103. package/dist/orchestrator.js +0 -707
  104. package/dist/orchestrator.js.map +0 -1
  105. package/dist/orchestrator.test.d.ts +0 -2
  106. package/dist/orchestrator.test.d.ts.map +0 -1
  107. package/dist/orchestrator.test.js +0 -55
  108. package/dist/orchestrator.test.js.map +0 -1
  109. package/dist/pixel-WB6VRJWP.js +0 -13
  110. package/dist/radio-picker.d.ts +0 -20
  111. package/dist/radio-picker.d.ts.map +0 -1
  112. package/dist/radio-picker.js +0 -31
  113. package/dist/radio-picker.js.map +0 -1
  114. package/dist/radio.d.ts +0 -43
  115. package/dist/radio.d.ts.map +0 -1
  116. package/dist/radio.js +0 -150
  117. package/dist/radio.js.map +0 -1
  118. package/dist/sessions.d.ts +0 -21
  119. package/dist/sessions.d.ts.map +0 -1
  120. package/dist/sessions.js +0 -122
  121. package/dist/sessions.js.map +0 -1
  122. package/dist/settings.d.ts +0 -11
  123. package/dist/settings.d.ts.map +0 -1
  124. package/dist/settings.js +0 -38
  125. package/dist/settings.js.map +0 -1
  126. package/dist/slash-commands.d.ts +0 -19
  127. package/dist/slash-commands.d.ts.map +0 -1
  128. package/dist/slash-commands.js +0 -76
  129. package/dist/slash-commands.js.map +0 -1
  130. package/dist/splash.d.ts +0 -21
  131. package/dist/splash.d.ts.map +0 -1
  132. package/dist/splash.js +0 -137
  133. package/dist/splash.js.map +0 -1
  134. package/dist/task-tools.d.ts +0 -18
  135. package/dist/task-tools.d.ts.map +0 -1
  136. package/dist/task-tools.js +0 -172
  137. package/dist/task-tools.js.map +0 -1
  138. package/dist/tasks-store.d.ts +0 -66
  139. package/dist/tasks-store.d.ts.map +0 -1
  140. package/dist/tasks-store.js +0 -199
  141. package/dist/tasks-store.js.map +0 -1
  142. package/dist/tasks-store.test.d.ts +0 -2
  143. package/dist/tasks-store.test.d.ts.map +0 -1
  144. package/dist/tasks-store.test.js +0 -138
  145. package/dist/tasks-store.test.js.map +0 -1
  146. package/dist/tool-formatters.d.ts +0 -7
  147. package/dist/tool-formatters.d.ts.map +0 -1
  148. package/dist/tool-formatters.js +0 -111
  149. package/dist/tool-formatters.js.map +0 -1
  150. package/dist/tools.d.ts +0 -26
  151. package/dist/tools.d.ts.map +0 -1
  152. package/dist/tools.js +0 -133
  153. package/dist/tools.js.map +0 -1
  154. package/dist/types.d.ts +0 -32
  155. package/dist/types.d.ts.map +0 -1
  156. package/dist/types.js +0 -2
  157. package/dist/types.js.map +0 -1
  158. package/dist/worker.d.ts +0 -47
  159. package/dist/worker.d.ts.map +0 -1
  160. package/dist/worker.js +0 -123
  161. package/dist/worker.js.map +0 -1
  162. /package/dist/{pixel-WB6VRJWP.js.map → pixel-WPYTQADG.js.map} +0 -0
@@ -1,599 +0,0 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
- import { Box, Static, Text, render, useApp, useInput, useStdout } from "ink";
4
- import { ThemeContext, loadTheme, useTheme } from "@kenkaiiii/ggcoder/ui/theme";
5
- import { ActivityIndicator, AnimationProvider, AssistantMessage, CompactionDone, CompactionSpinner, InputArea, MessageResponse, ModelSelector, StreamingArea, ToolExecution, ToolUseLoader, UserMessage, useAnimationActive, useAnimationTick, } from "@kenkaiiii/ggcoder/ui";
6
- import { useDoublePress } from "@kenkaiiii/ggcoder/ui/hooks/double-press";
7
- import { TerminalSizeProvider, useTerminalSize } from "@kenkaiiii/ggcoder/ui/hooks/terminal-size";
8
- import { BossFooter } from "./boss-footer.js";
9
- import { BossBanner } from "./banner.js";
10
- import { bossStore, useBossState } from "./boss-store.js";
11
- import { BOSS_SLASH_COMMANDS, canonicalName, parseSlash, buildHelpText } from "./slash-commands.js";
12
- import { bossToolFormatters } from "./tool-formatters.js";
13
- import { projectColor } from "./colors.js";
14
- import { BOSS_PHRASES } from "./boss-phrases.js";
15
- import { COLORS, PULSE_COLORS as BOSS_PULSE_COLORS } from "./branding.js";
16
- import { BossTasksOverlay } from "./boss-tasks-overlay.js";
17
- import { VERSION } from "./branding.js";
18
- import { RadioPicker } from "./radio-picker.js";
19
- import { getCurrentStation, playRadio, stopRadio, RADIO_STATIONS } from "./radio.js";
20
- import { getPendingUpdate, startPeriodicUpdateCheck, stopPeriodicUpdateCheck, } from "./auto-update.js";
21
- export function BossApp({ boss }) {
22
- const theme = loadTheme("dark");
23
- return (_jsx(TerminalSizeProvider, { children: _jsx(ThemeContext.Provider, { value: theme, children: _jsx(AnimationProvider, { children: _jsx(BossAppInner, { boss: boss }) }) }) }));
24
- }
25
- function BossAppInner({ boss }) {
26
- const state = useBossState();
27
- const { exit } = useApp();
28
- const { stdout } = useStdout();
29
- const { resizeKey, columns } = useTerminalSize();
30
- const runStartRef = useRef(null);
31
- runStartRef.current = state.runStartMs;
32
- // Live char count of the current streaming text — drives ActivityIndicator's
33
- // smooth token-counter animation between turn_end events.
34
- const charCountRef = useRef(0);
35
- charCountRef.current = state.streaming?.text.length ?? 0;
36
- // Accumulated real input tokens across completed turns — used alongside
37
- // charCountRef so the counter interpolates smoothly between hard updates.
38
- const realTokensAccumRef = useRef(0);
39
- realTokensAccumRef.current = state.bossInputTokens;
40
- // Track the most recent user message so the activity bar's contextual phrase
41
- // selection has something to riff on (when not using BOSS_PHRASES override).
42
- const [lastUserMessage, setLastUserMessage] = useState("");
43
- const [overlay, setOverlay] = useState(null);
44
- // Track the currently-playing station id so the picker can mark it with *
45
- // and so we have a reactive value for any future "now playing" indicator.
46
- // Seeded from the radio module's module-level state — usually null on
47
- // launch but resilient to a hot-restart of the React tree.
48
- const [currentRadio, setCurrentRadio] = useState(() => getCurrentStation());
49
- // Reserved for future Static-remount triggers (e.g. theme switches). Not
50
- // currently bumped — /clear no longer touches Static state directly.
51
- const [staticKey] = useState(0);
52
- // Auto-update indicator: true when a newer version of @kenkaiiii/gg-boss
53
- // is on disk waiting for the next restart. Seeded synchronously from the
54
- // state file (so we show the indicator immediately if a previous session
55
- // queued one) and bumped to true by the periodic check below if a fresh
56
- // version drops mid-session.
57
- const [updatePending, setUpdatePending] = useState(() => getPendingUpdate(VERSION) !== null);
58
- // Periodic in-session check — pings npm every hour while the session is
59
- // alive. If a newer version arrives, we set updatePending so the worker
60
- // bar shows the "✨ Update ready · restart to apply" hint, AND drop a
61
- // friendly info row into chat so the user sees the news immediately.
62
- useEffect(() => {
63
- startPeriodicUpdateCheck(VERSION, (msg) => {
64
- bossStore.appendInfo(msg, "info");
65
- setUpdatePending(true);
66
- });
67
- return () => stopPeriodicUpdateCheck();
68
- }, []);
69
- // Terminal title — dynamically reflects worker activity so the user can
70
- // glance at the tab/window from another app and see how many workers are
71
- // still running. OSC 0 sets both window and tab title in most modern
72
- // terminals (Ghostty, Terminal.app, iTerm2, Kitty).
73
- //
74
- // States:
75
- // N workers running "● 5 workers running · GG Boss"
76
- // 1 worker running "● 1 worker running · GG Boss"
77
- // boss thinking only "● GG Boss"
78
- // idle "GG Boss"
79
- const workersRunning = state.workers.filter((w) => w.status === "working").length;
80
- const titlePrevRef = useRef("");
81
- useEffect(() => {
82
- if (!stdout)
83
- return;
84
- let title;
85
- if (workersRunning > 0) {
86
- const label = `${workersRunning} worker${workersRunning === 1 ? "" : "s"} running`;
87
- title = `● ${label} · GG Boss`;
88
- }
89
- else if (state.phase === "working") {
90
- title = "● GG Boss";
91
- }
92
- else {
93
- title = "GG Boss";
94
- }
95
- if (title !== titlePrevRef.current) {
96
- titlePrevRef.current = title;
97
- stdout.write(`\x1b]0;${title}\x1b\\`);
98
- }
99
- }, [stdout, workersRunning, state.phase]);
100
- useEffect(() => {
101
- return () => {
102
- stdout?.write(`\x1b]0;GG Boss\x1b\\`);
103
- };
104
- }, [stdout]);
105
- const staticItems = useMemo(() => [{ kind: "banner", id: "banner" }, ...state.history], [state.history]);
106
- /**
107
- * No screen clears, no Static remounts. Just toggle React state.
108
- *
109
- * Banner is emitted ONCE on initial mount (Static's natural behavior — items
110
- * by id are emitted exactly once per Static instance lifetime). It lives in
111
- * scrollback forever after that, never re-emitted. So duplicate banners are
112
- * structurally impossible.
113
- *
114
- * The remaining concern is Ink's log-update cursor math when the live area
115
- * shrinks (tasks pane → chat chrome). log-update only clears within the
116
- * previous frame's footprint at the bottom of the viewport — it cannot
117
- * reach into scrollback. So banner + history in scrollback stay intact.
118
- */
119
- const openOverlay = useCallback((next) => {
120
- setOverlay(next);
121
- }, []);
122
- const closeOverlay = useCallback(() => {
123
- setOverlay(null);
124
- }, []);
125
- void stdout;
126
- // ggcoder's double-press pattern: 800ms window. First press shows
127
- // "Press Ctrl+C again to exit" in the footer; second within 800ms exits.
128
- const handleDoubleExit = useDoublePress((pending) => bossStore.setExitPending(pending), () => exit());
129
- // Two-phase flush — see boss-store.ts for the rationale. Phase 1 (orchestrator
130
- // pushes into pendingFlush, live area shrinks) already happened; phase 2 here
131
- // commits to history on the next render so Ink doesn't clip long responses.
132
- useEffect(() => {
133
- if (state.pendingFlush.length > 0) {
134
- bossStore.commitPendingFlush();
135
- }
136
- }, [state.flushGeneration, state.pendingFlush.length]);
137
- // ── App-level keyboard ──────────────────────────────────
138
- // ESC: abort current boss call when working (InputArea handles otherwise).
139
- // Ctrl+T: toggle the Tasks overlay (matches ggcoder's keybind).
140
- useInput((input, key) => {
141
- if (key.ctrl && input === "t") {
142
- if (overlay === "tasks")
143
- closeOverlay();
144
- else
145
- openOverlay("tasks");
146
- return;
147
- }
148
- if (key.escape && state.phase === "working") {
149
- boss.abort();
150
- }
151
- });
152
- const handleSlashCommand = async (value) => {
153
- const parsed = parseSlash(value);
154
- if (!parsed)
155
- return false;
156
- const name = canonicalName(parsed.name);
157
- if (!name) {
158
- bossStore.appendInfo(`Unknown command: /${parsed.name}`, "warning");
159
- return true;
160
- }
161
- switch (name) {
162
- case "help":
163
- bossStore.appendUser(value);
164
- // Render help via an assistant block so Markdown formatting + dot prefix.
165
- bossStore.appendInfo(buildHelpText(), "info");
166
- return true;
167
- case "clear":
168
- // Reset React state + agent context but DO NOT touch the terminal
169
- // (no ANSI clear, no Static remount). Disturbing Ink's log-update
170
- // cursor tracking after a render is in flight breaks the live-area
171
- // positioning — the input ends up drawn above the messages and new
172
- // content scrolls out of sight. The user's prior chat remains in
173
- // scrollback (they can scroll up to see it) but the boss starts a
174
- // fresh conversation. The info row gives visual confirmation that
175
- // the action took effect.
176
- bossStore.clearHistory();
177
- await boss.resetConversation();
178
- bossStore.appendInfo("Session cleared.", "info");
179
- return true;
180
- case "model-boss":
181
- openOverlay("model-boss");
182
- return true;
183
- case "model-workers":
184
- openOverlay("model-workers");
185
- return true;
186
- case "compact":
187
- bossStore.appendUser(value);
188
- await boss.manualCompact();
189
- return true;
190
- case "radio":
191
- openOverlay("radio");
192
- return true;
193
- case "quit":
194
- exit();
195
- return true;
196
- }
197
- return false;
198
- };
199
- const handleModelSelect = (value) => {
200
- const colon = value.indexOf(":");
201
- if (colon < 0) {
202
- closeOverlay();
203
- return;
204
- }
205
- const provider = value.slice(0, colon);
206
- const model = value.slice(colon + 1);
207
- if (overlay === "model-boss") {
208
- void boss.switchBossModel(provider, model);
209
- }
210
- else if (overlay === "model-workers") {
211
- void boss.switchWorkerModel(provider, model);
212
- }
213
- closeOverlay();
214
- };
215
- const handleSubmit = (value) => {
216
- const trimmed = value.trim();
217
- if (!trimmed)
218
- return;
219
- if (trimmed.startsWith("/") && !trimmed.startsWith("//")) {
220
- void handleSlashCommand(trimmed);
221
- return;
222
- }
223
- // Show the user's literal text in chat history.
224
- bossStore.appendUser(trimmed);
225
- setLastUserMessage(trimmed);
226
- // Inject the scope pill into the message the boss actually sees, so the
227
- // user doesn't have to write "for the yaatuber project, …" every prompt.
228
- const scoped = scopePrefix(state.scope) + trimmed;
229
- boss.enqueueUserMessage(scoped);
230
- };
231
- const handleAbort = () => {
232
- // Ctrl+C while boss is running → single-press abort (matches ggcoder).
233
- if (state.phase === "working") {
234
- boss.abort();
235
- return;
236
- }
237
- // Boss is idle → double-press to exit, with footer pending message.
238
- handleDoubleExit();
239
- };
240
- return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(Static, { items: staticItems, style: { width: "100%" }, children: (item) => (_jsx(Box, { flexDirection: "column", paddingRight: 1, children: _jsx(StaticRowView, { row: item }) }, item.id)) }, `${resizeKey}-${staticKey}`), overlay === "tasks" ? (_jsx(BossTasksOverlay, { boss: boss, workers: state.workers, onClose: closeOverlay })) : (_jsxs(_Fragment, { children: [state.streaming && (_jsx(StreamingTurnView, { turn: state.streaming, isRunning: state.phase === "working" })), state.phase === "working" && (_jsx(Box, { marginTop: 1, children: _jsx(ActivityIndicator, { phase: state.activityPhase, elapsedMs: state.runStartMs ? Date.now() - state.runStartMs : 0, runStartRef: runStartRef, thinkingMs: state.streaming?.thinkingMs ?? 0, isThinking: state.activityPhase === "thinking", tokenEstimate: state.bossInputTokens, charCountRef: charCountRef, realTokensAccumRef: realTokensAccumRef, userMessage: lastUserMessage, activeToolNames: (state.streaming?.tools ?? [])
241
- .filter((t) => t.status === "running")
242
- .map((t) => t.name), retryInfo: state.retryInfo, phrases: BOSS_PHRASES, pulseColors: BOSS_PULSE_COLORS }) })), state.compaction?.state === "running" && _jsx(CompactionSpinner, {}), state.compaction?.state === "done" && (_jsx(CompactionDone, { originalCount: state.compaction.originalCount, newCount: state.compaction.newCount, tokensBefore: state.compaction.tokensBefore, tokensAfter: state.compaction.tokensAfter })), _jsx(InputArea, { onSubmit: handleSubmit, onAbort: handleAbort, disabled: state.phase === "working", isActive: !overlay, cwd: process.cwd(), commands: BOSS_SLASH_COMMANDS, scopeBadge: _jsx(ScopePill, { scope: state.scope }),
243
- // Mouse-tracking escape sequences cause Ghostty to emit phantom
244
- // pastes of the system clipboard during high-frequency UI updates
245
- // (workers running, shimmer animating). gg-boss's UI updates a lot,
246
- // so we forfeit click-to-position-cursor in the input to keep
247
- // the clipboard from leaking into the chat field.
248
- disableMouseTracking: true, onTab: () => bossStore.cycleScope(), onShiftTab: () => {
249
- // Don't appendInfo — Static lives outside the overlay branch, so
250
- // any history row added here renders in scrollback above the
251
- // tasks pane and looks like it's inside it. The footer already
252
- // shows live "Thinking on/off" — that's the indicator.
253
- const next = state.bossThinkingLevel ? undefined : "medium";
254
- void boss.setBossThinking(next);
255
- } }), overlay === "model-boss" || overlay === "model-workers" ? (_jsx(ModelSelector, { onSelect: handleModelSelect, onCancel: closeOverlay, loggedInProviders: state.loggedInProviders, currentModel: overlay === "model-boss" ? state.bossModel : state.workerModel, currentProvider: overlay === "model-boss" ? state.bossProvider : state.workerProvider })) : overlay === "radio" ? (_jsx(RadioPicker, { currentStationId: currentRadio, onCancel: closeOverlay, onSelect: (value) => {
256
- if (value === "off") {
257
- stopRadio();
258
- setCurrentRadio(null);
259
- bossStore.appendInfo("Radio off.", "info");
260
- }
261
- else {
262
- const result = playRadio(value);
263
- if (result.ok) {
264
- setCurrentRadio(value);
265
- const station = RADIO_STATIONS.find((s) => s.id === value);
266
- bossStore.appendInfo(`Now playing: ${station?.name ?? value}`, "info");
267
- }
268
- else {
269
- bossStore.appendInfo(result.error ?? "Radio failed to start.", "warning");
270
- }
271
- }
272
- closeOverlay();
273
- } })) : (_jsxs(_Fragment, { children: [_jsx(BossFooter, { bossModel: state.bossModel, workerModel: state.workerModel, tokensIn: state.bossInputTokens, exitPending: state.exitPending, bossThinkingLevel: state.bossThinkingLevel, updatePending: updatePending, currentRadioStationId: currentRadio }), !state.exitPending && (_jsx(WorkerStatusBar, { workers: state.workers, pendingMessages: state.pendingUserMessages }))] }))] }))] }));
274
- }
275
- // ── Scope pill (gg-boss specific) ──────────────────────────
276
- function ScopePill({ scope }) {
277
- const theme = useTheme();
278
- const isAll = scope === "all";
279
- // "All" → boss accent (fuchsia) so multi-project mode wears the brand.
280
- // Specific project → its stable project color so the pill matches its
281
- // appearances elsewhere in the TUI.
282
- const bg = isAll ? COLORS.accent : projectColor(scope);
283
- const label = isAll ? "All" : scope;
284
- // Black text reads cleanly on every color in the palette — the project hues
285
- // are deliberately light/saturated, which is unreadable with white on top.
286
- return (_jsxs(Text, { children: [_jsx(Text, { color: theme.textDim, children: "Project " }), _jsx(Text, { color: "black", backgroundColor: bg, bold: true, children: ` ${label} ` }), _jsxs(Text, { color: theme.textDim, children: [" ", _jsx(Text, { color: theme.primary, children: "Tab" }), " to switch"] })] }));
287
- }
288
- /**
289
- * Prepend the active scope to the user's message before it reaches the boss.
290
- * Boss's system prompt teaches it to interpret these prefixes.
291
- */
292
- function scopePrefix(scope) {
293
- if (scope === "all")
294
- return "[scope:all] ";
295
- return `[scope:${scope}] `;
296
- }
297
- // ── Worker status row (gg-boss specific) ───────────────────
298
- const SHIMMER_WIDTH = 3;
299
- function formatElapsed(ms) {
300
- const total = Math.floor(ms / 1000);
301
- const m = Math.floor(total / 60);
302
- const s = total % 60;
303
- return `${m}:${s.toString().padStart(2, "0")}`;
304
- }
305
- /**
306
- * Mount this when (and only when) the shimmer needs to tick. AnimationProvider
307
- * stops the global timer when its subscriber count hits zero, so unmounting
308
- * this sentinel halts the 10Hz re-render loop while every worker is idle.
309
- */
310
- function AnimationActiveSentinel() {
311
- useAnimationActive();
312
- return null;
313
- }
314
- /**
315
- * Same shimmer pattern used by ggcoder's ActivityIndicator phrases — a bright
316
- * highlight band of width `SHIMMER_WIDTH` slides across the text while the
317
- * rest stays dim. Driven by the global animation tick.
318
- */
319
- function ShimmerName({ name, color, tick, }) {
320
- // Cycle covers the name length plus a SHIMMER_WIDTH-wide pre-roll/post-roll
321
- // so the bright band fully exits one side before re-entering the other.
322
- const cycle = name.length + SHIMMER_WIDTH * 2;
323
- const shimmerPos = (tick % cycle) - SHIMMER_WIDTH;
324
- return (_jsx(Text, { children: name.split("").map((ch, i) => {
325
- const isBright = Math.abs(i - shimmerPos) <= SHIMMER_WIDTH;
326
- return (_jsx(Text, { color: color, bold: isBright, dimColor: !isBright, children: ch }, i));
327
- }) }));
328
- }
329
- function WorkerStatusBar({ workers, pendingMessages, }) {
330
- const theme = useTheme();
331
- // Active-first layout: only working and errored workers get named slots.
332
- // Idle workers collapse into a single "+N idle" trailer so the bar scales
333
- // cleanly from 5 projects to 50. With 4 of 50 projects active, you see
334
- // four shimmering names + "+46 idle" instead of fifty repeated glyphs.
335
- const working = workers.filter((w) => w.status === "working");
336
- const errored = workers.filter((w) => w.status === "error");
337
- const idleCount = workers.length - working.length - errored.length;
338
- const anyWorking = working.length > 0;
339
- // Passive tick consumer — when no Sentinel is mounted (no working worker),
340
- // the global timer is paused and the tick value stops changing, so this
341
- // component doesn't re-render at 10Hz when everything is idle.
342
- const tick = useAnimationTick();
343
- const now = Date.now();
344
- if (workers.length === 0)
345
- return null;
346
- // Render order: working (shimmer + timer) → errored (✗ + name) → idle
347
- // count (dim "N idle"). The shimmer + project hue already announce
348
- // "active" — no need for a leading ● dot. The errored ✗ stays because
349
- // colour alone isn't enough to call out a stuck worker. The idle slot
350
- // keeps the ○ as a glyph-only quantifier ("○ 17"). Separator: thin
351
- // vertical bar, matching the footer's style.
352
- const slots = [];
353
- for (const w of working) {
354
- const projectHue = projectColor(w.name);
355
- const elapsed = w.workStartedAt ? formatElapsed(now - w.workStartedAt) : null;
356
- slots.push(_jsxs(React.Fragment, { children: [_jsx(ShimmerName, { name: w.name, color: projectHue, tick: tick }), elapsed && _jsxs(Text, { color: theme.textDim, children: [" ", elapsed] })] }, `w-${w.name}`));
357
- }
358
- for (const w of errored) {
359
- slots.push(_jsx(React.Fragment, { children: _jsxs(Text, { color: theme.error, children: ["\u2717 ", w.name] }) }, `e-${w.name}`));
360
- }
361
- if (idleCount > 0) {
362
- slots.push(_jsx(React.Fragment, { children: _jsxs(Text, { color: theme.textDim, children: ["\u25CB ", idleCount, " idle"] }) }, "idle"));
363
- }
364
- return (_jsxs(Box, { paddingX: 1, children: [anyWorking && _jsx(AnimationActiveSentinel, {}), slots.map((slot, i) => (_jsxs(React.Fragment, { children: [i > 0 && _jsx(Text, { color: theme.border, children: " │ " }), slot] }, i))), pendingMessages > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.textDim, children: " " }), _jsxs(Text, { color: theme.warning, children: [pendingMessages, " message", pendingMessages === 1 ? "" : "s", " queued"] })] }))] }));
365
- }
366
- // ── Row dispatch ───────────────────────────────────────────
367
- function StaticRowView({ row }) {
368
- if (row.kind === "banner") {
369
- return (_jsx(Box, { paddingX: 1, children: _jsx(BossBanner, { subtitle: "Orchestrator", showShortcuts: true }) }));
370
- }
371
- if (row.kind === "user")
372
- return _jsx(UserMessage, { text: row.text });
373
- if (row.kind === "assistant")
374
- return _jsx(AssistantRow, { item: row });
375
- if (row.kind === "tool")
376
- return _jsx(ToolHistoryRow, { item: row });
377
- if (row.kind === "worker_event")
378
- return _jsx(WorkerEventRow, { item: row });
379
- if (row.kind === "worker_error")
380
- return _jsx(WorkerErrorRow, { item: row });
381
- if (row.kind === "info")
382
- return _jsx(InfoRow, { text: row.text, level: row.level ?? "info" });
383
- if (row.kind === "task_dispatch")
384
- return _jsx(TaskDispatchRow, { tasks: row.tasks });
385
- return null;
386
- }
387
- function TaskDispatchRow({ tasks, }) {
388
- const theme = useTheme();
389
- const count = tasks.length;
390
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, marginTop: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: COLORS.primary, bold: true, children: "⏺ " }), _jsxs(Text, { color: theme.text, bold: true, children: ["Running ", count, " task", count === 1 ? "" : "s", ":"] })] }), tasks.map((t, i) => (_jsxs(Text, { children: [_jsx(Text, { color: theme.textDim, children: " • " }), _jsx(Text, { color: projectColor(t.project), bold: true, children: t.project }), _jsx(Text, { color: theme.textDim, children: ": " }), _jsx(Text, { color: theme.text, children: t.title })] }, `${t.project}-${i}`)))] }));
391
- }
392
- /**
393
- * Auto-highlight common keyboard shortcuts in any boss-written prose by
394
- * wrapping them in backticks before passing to the Markdown renderer (which
395
- * styles inline code with a distinctive color/background). Catches things
396
- * like Ctrl+T, Shift+Tab, Cmd+K, Esc, F-keys, arrow-key combos. The boss may
397
- * already wrap them itself — these regexes deliberately skip text that's
398
- * already inside backticks (or fenced blocks) so we don't double-wrap.
399
- */
400
- const SHORTCUT_PATTERNS = [
401
- // Modifier+Key combos: Ctrl+T, Shift+Tab, Cmd+K, Ctrl+Shift+P, Ctrl+C
402
- /\b(?:Ctrl|Cmd|Alt|Option|Opt|Shift|Meta|Win|Super)(?:\s*\+\s*(?:Ctrl|Cmd|Alt|Option|Opt|Shift|Meta|Win|Super))*\s*\+\s*(?:Tab|Enter|Esc|Escape|Space|Backspace|Delete|Del|Home|End|PageUp|PageDown|Up|Down|Left|Right|F[1-9]|F1[0-2]|[A-Z0-9]|\/|\?|\.|,|;|=|-)\b/g,
403
- // Bare named keys (only when surrounded by clear key context)
404
- /\b(?:Ctrl-[A-Z]|F[1-9]|F1[0-2])\b/g,
405
- ];
406
- function highlightShortcuts(text) {
407
- if (!text)
408
- return text;
409
- // Mask code spans + fenced blocks so we don't try to re-wrap shortcuts that
410
- // are already in backtick territory. The sentinel uses a private-use unicode
411
- // codepoint so it can't realistically collide with anything the boss writes.
412
- const SENTINEL = "";
413
- const masks = [];
414
- let masked = text.replace(/```[\s\S]*?```|`[^`]+`/g, (m) => {
415
- const idx = masks.push(m) - 1;
416
- return `${SENTINEL}${idx}${SENTINEL}`;
417
- });
418
- for (const re of SHORTCUT_PATTERNS) {
419
- masked = masked.replace(re, (m) => `\`${m}\``);
420
- }
421
- return masked.replace(new RegExp(`${SENTINEL}(\\d+)${SENTINEL}`, "g"), (_, i) => masks[Number(i)]);
422
- }
423
- function AssistantRow({ item }) {
424
- return (_jsx(AssistantMessage, { text: highlightShortcuts(item.text), thinking: item.thinking, thinkingMs: item.thinkingMs }));
425
- }
426
- function ToolHistoryRow({ item }) {
427
- return (_jsx(ToolExecution, { status: "done", name: item.name, args: item.args, result: item.result, isError: item.isError, details: item.details, formatters: bossToolFormatters }));
428
- }
429
- /**
430
- * Pull the `Status:` line out of a worker's final text (the brief in
431
- * tools.ts asks every worker to end with one of: DONE | UNVERIFIED |
432
- * PARTIAL | BLOCKED | INFO). Returns null if the line is missing or invalid.
433
- */
434
- function parseStatusGrade(text) {
435
- // Use the LAST occurrence of "Status: X" (some workers explain status
436
- // mid-text and re-emit it in the trailer). Also accept anything after the
437
- // grade word — workers occasionally write "Status: INFO — trailing comment"
438
- // which the previous end-of-line anchor would have rejected.
439
- const matches = [...text.matchAll(/^\s*Status:\s*(DONE|UNVERIFIED|PARTIAL|BLOCKED|INFO)\b/gim)];
440
- const last = matches[matches.length - 1];
441
- if (!last)
442
- return null;
443
- return last[1].toUpperCase();
444
- }
445
- /**
446
- * Pull the structured fields out of the worker's reply trailer (appended by
447
- * WORKER_PROMPT_BRIEF). Each field is captured up to (but not including) the
448
- * next field marker or end-of-text.
449
- */
450
- function parseWorkerTrailer(text) {
451
- const out = {};
452
- const grab = (label) => {
453
- // Match "Label: value" up to the next "Label:" line or end. Multi-line.
454
- const re = new RegExp(`^\\s*${label}:\\s*([\\s\\S]*?)(?=^\\s*(?:Changed|Skipped|Verified|Notes|Status):|$)`, "im");
455
- const m = re.exec(text);
456
- if (!m)
457
- return undefined;
458
- const v = m[1]
459
- .replace(/```[\s\S]*?```/g, "[code]")
460
- .replace(/`([^`]+)`/g, "$1")
461
- .replace(/\s+/g, " ")
462
- .trim();
463
- return v.length > 0 ? v : undefined;
464
- };
465
- out.changed = grab("Changed");
466
- out.skipped = grab("Skipped");
467
- out.verified = grab("Verified");
468
- out.notes = grab("Notes");
469
- return out;
470
- }
471
- function clip(text, maxLen) {
472
- return text.length <= maxLen ? text : text.slice(0, Math.max(1, maxLen - 1)) + "…";
473
- }
474
- /**
475
- * Build a one-line summary from the trailer. Prefers the substantive fields
476
- * (Changed, Verified, Notes) that actually tell the user what happened — not
477
- * the worker's preamble like "I'll start by detecting...". Falls back to
478
- * first-sentence-of-preamble only when the trailer is empty (non-conforming
479
- * worker reply).
480
- */
481
- function summarizeFinalText(text, maxLen) {
482
- if (!text)
483
- return "";
484
- const trailer = parseWorkerTrailer(text);
485
- const parts = [];
486
- if (trailer.changed)
487
- parts.push(`Changed: ${trailer.changed}`);
488
- if (trailer.verified)
489
- parts.push(`Verified: ${trailer.verified}`);
490
- if (trailer.skipped)
491
- parts.push(`Skipped: ${trailer.skipped}`);
492
- if (trailer.notes)
493
- parts.push(`Notes: ${trailer.notes}`);
494
- if (parts.length > 0)
495
- return clip(parts.join(" · "), maxLen);
496
- // No trailer — fall back to the first sentence of the response body.
497
- const beforeSummary = text.split(/^Changed:|^Skipped:|^Verified:|^Notes:|^Status:/im)[0];
498
- const stripped = beforeSummary
499
- .replace(/```[\s\S]*?```/g, "[code]")
500
- .replace(/`([^`]+)`/g, "$1")
501
- .replace(/\*\*([^*]+)\*\*/g, "$1")
502
- .replace(/\*([^*]+)\*/g, "$1")
503
- .replace(/^\s*[-*]\s+/gm, "")
504
- .replace(/^#+\s+/gm, "")
505
- .replace(/\s+/g, " ")
506
- .trim();
507
- if (!stripped)
508
- return "";
509
- const firstSentence = stripped.match(/^[^.!?\n]+[.!?]/);
510
- return clip(firstSentence ? firstSentence[0] : stripped, maxLen);
511
- }
512
- function statusGradeColor(grade, theme) {
513
- switch (grade) {
514
- case "DONE":
515
- return theme.success;
516
- case "UNVERIFIED":
517
- case "PARTIAL":
518
- return theme.warning;
519
- case "BLOCKED":
520
- return theme.error;
521
- case "INFO":
522
- return theme.textDim;
523
- default:
524
- return theme.textDim;
525
- }
526
- }
527
- function WorkerEventRow({ item }) {
528
- const theme = useTheme();
529
- const { columns } = useTerminalSize();
530
- const failedCount = item.toolsUsed.filter((t) => !t.ok).length;
531
- const total = item.toolsUsed.length;
532
- const grade = parseStatusGrade(item.finalText);
533
- // Loader status: prefer the worker's self-reported grade. Fall back to
534
- // tool-error count if the worker omitted Status (older runs / non-conforming).
535
- const loaderStatus = grade === "BLOCKED" || failedCount > 0
536
- ? "error"
537
- : grade === "UNVERIFIED" || grade === "PARTIAL"
538
- ? "queued"
539
- : "done";
540
- // Errors override the project hue with red; otherwise the project gets its
541
- // stable color so successive turns from the same worker visually cluster.
542
- const headerColor = loaderStatus === "error" ? theme.toolError : projectColor(item.project);
543
- const toolSummary = total === 0
544
- ? "no tools"
545
- : failedCount > 0
546
- ? `${total} tools (${failedCount} failed)`
547
- : `${total} tool${total === 1 ? "" : "s"}`;
548
- // MessageResponse uses 6 chars for " ⎿ " gutter; reserve a few more for
549
- // safety. Each trailer field renders on its own line so users can scan
550
- // Changed / Verified / Notes independently rather than a single squished line.
551
- const fieldMaxLen = Math.max(20, columns - 14);
552
- const trailer = parseWorkerTrailer(item.finalText);
553
- const hasTrailer = !!(trailer.changed || trailer.skipped || trailer.verified || trailer.notes);
554
- const fallbackSummary = hasTrailer ? "" : summarizeFinalText(item.finalText, fieldMaxLen);
555
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(ToolUseLoader, { status: loaderStatus }), _jsx(Box, { flexGrow: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: headerColor, bold: true, children: item.project }), _jsx(Text, { color: theme.text, children: ` turn ${item.turnIndex}` }), _jsx(Text, { color: theme.textDim, children: ` · ${toolSummary}` }), grade && (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.textDim, children: " · " }), _jsx(Text, { color: statusGradeColor(grade, theme), bold: true, children: grade })] }))] }) })] }), hasTrailer ? (_jsxs(_Fragment, { children: [trailer.changed && (_jsx(TrailerLine, { label: "Changed", value: trailer.changed, maxLen: fieldMaxLen })), trailer.verified && (_jsx(TrailerLine, { label: "Verified", value: trailer.verified, maxLen: fieldMaxLen, labelColor: theme.success })), trailer.skipped && (_jsx(TrailerLine, { label: "Skipped", value: trailer.skipped, maxLen: fieldMaxLen, labelColor: theme.warning })), trailer.notes && (_jsx(TrailerLine, { label: "Notes", value: trailer.notes, maxLen: fieldMaxLen }))] })) : (fallbackSummary && (_jsx(MessageResponse, { children: _jsx(Text, { color: theme.textDim, wrap: "truncate", children: fallbackSummary }) })))] }));
556
- }
557
- function TrailerLine({ label, value, maxLen, labelColor, }) {
558
- const theme = useTheme();
559
- return (_jsx(MessageResponse, { children: _jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: labelColor ?? theme.textDim, bold: true, children: [label, ":"] }), _jsx(Text, { color: theme.text, children: ` ${clip(value, maxLen - label.length - 2)}` })] }) }));
560
- }
561
- function WorkerErrorRow({ item }) {
562
- const theme = useTheme();
563
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(ToolUseLoader, { status: "error" }), _jsx(Box, { flexGrow: 1, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: theme.toolError, bold: true, children: item.project }), _jsx(Text, { color: theme.textDim, children: " worker error" })] }) })] }), _jsx(MessageResponse, { children: _jsx(Text, { color: theme.error, wrap: "wrap", children: item.message }) })] }));
564
- }
565
- function InfoRow({ text, level, }) {
566
- // info → render through AssistantMessage so it gets the dot + Markdown.
567
- if (level === "info")
568
- return _jsx(AssistantMessage, { text: text });
569
- // warning / error → match the ToolUseLoader chrome so the row reads as a
570
- // first-class event (consistent with worker errors / failed tool calls)
571
- // rather than bare colored text.
572
- const theme = useTheme();
573
- const color = level === "error" ? theme.error : theme.warning;
574
- return (_jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(ToolUseLoader, { status: level === "error" ? "error" : "queued" }), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { color: color, wrap: "wrap", children: text }) })] }));
575
- }
576
- // ── Streaming (live) ───────────────────────────────────────
577
- function StreamingTurnView({ turn, isRunning, }) {
578
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(StreamingArea, { isRunning: isRunning, streamingText: turn.text, streamingThinking: turn.thinking, thinkingMs: turn.thinkingMs }), turn.tools.map((t) => (_jsx(StreamingToolRow, { tool: t }, t.toolCallId)))] }));
579
- }
580
- function StreamingToolRow({ tool }) {
581
- if (tool.status === "running") {
582
- return (_jsx(ToolExecution, { status: "running", name: tool.name, args: tool.args, formatters: bossToolFormatters }));
583
- }
584
- return (_jsx(ToolExecution, { status: "done", name: tool.name, args: tool.args, result: tool.result ?? "", isError: tool.status === "error", details: tool.details, formatters: bossToolFormatters }));
585
- }
586
- export function renderBossApp(opts) {
587
- // Disable Ink's built-in exit-on-Ctrl+C — we need our own double-press
588
- // handler in BossApp to drive the "Press Ctrl+C again to exit" footer
589
- // message. With this flag true (the default), Ink kills the process on the
590
- // very first Ctrl+C and InputArea's onAbort never runs.
591
- const instance = render(_jsx(BossApp, { boss: opts.boss }), { exitOnCtrlC: false });
592
- return {
593
- waitUntilExit: async () => {
594
- await instance.waitUntilExit();
595
- },
596
- unmount: () => instance.unmount(),
597
- };
598
- }
599
- //# sourceMappingURL=orchestrator-app.js.map