@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.
- package/dist/{chunk-JVQDTPYR.js → chunk-3EWLK53W.js} +18 -9
- package/dist/{chunk-JVQDTPYR.js.map → chunk-3EWLK53W.js.map} +1 -1
- package/dist/{chunk-5WNYQQPQ.js → chunk-QT366Y52.js} +4 -3
- package/dist/{chunk-5WNYQQPQ.js.map → chunk-QT366Y52.js.map} +1 -1
- package/dist/{chunk-PQAHDHVY.js → chunk-WJ4S4TOY.js} +3 -2
- package/dist/{chunk-PQAHDHVY.js.map → chunk-WJ4S4TOY.js.map} +1 -1
- package/dist/{chunk-B2WQ5E5J.js → chunk-YNWFCUMR.js} +2 -1
- package/dist/{chunk-B2WQ5E5J.js.map → chunk-YNWFCUMR.js.map} +1 -1
- package/dist/cli.js +2246 -182
- package/dist/cli.js.map +1 -1
- package/dist/{devtools-VBUDNGEI.js → devtools-4TI4D7F2.js} +3 -2
- package/dist/{devtools-VBUDNGEI.js.map → devtools-4TI4D7F2.js.map} +1 -1
- package/dist/{dist-7DAPKZGX.js → dist-VXOVSHZ5.js} +3 -2
- package/dist/{dist-7DAPKZGX.js.map → dist-VXOVSHZ5.js.map} +1 -1
- package/dist/{ignore-3AEIALHQ.js → ignore-76P4EAAU.js} +3 -2
- package/dist/{ignore-3AEIALHQ.js.map → ignore-76P4EAAU.js.map} +1 -1
- package/dist/index.js +21 -4
- package/dist/index.js.map +1 -1
- package/dist/{out-D65DTPFZ.js → out-XEXARMKS.js} +3 -2
- package/dist/{out-D65DTPFZ.js.map → out-XEXARMKS.js.map} +1 -1
- package/dist/pixel-WPYTQADG.js +14 -0
- package/dist/{pixel-fix-ALWXCLTS.js → pixel-fix-4WGZAJ5W.js} +4 -3
- package/dist/{pixel-fix-ALWXCLTS.js.map → pixel-fix-4WGZAJ5W.js.map} +1 -1
- package/package.json +10 -11
- package/dist/audio.d.ts +0 -21
- package/dist/audio.d.ts.map +0 -1
- package/dist/audio.js +0 -231
- package/dist/audio.js.map +0 -1
- package/dist/audio.test.d.ts +0 -2
- package/dist/audio.test.d.ts.map +0 -1
- package/dist/audio.test.js +0 -13
- package/dist/audio.test.js.map +0 -1
- package/dist/auto-update.d.ts +0 -24
- package/dist/auto-update.d.ts.map +0 -1
- package/dist/auto-update.js +0 -231
- package/dist/auto-update.js.map +0 -1
- package/dist/banner.d.ts +0 -17
- package/dist/banner.d.ts.map +0 -1
- package/dist/banner.js +0 -25
- package/dist/banner.js.map +0 -1
- package/dist/boss-footer.d.ts +0 -25
- package/dist/boss-footer.d.ts.map +0 -1
- package/dist/boss-footer.js +0 -107
- package/dist/boss-footer.js.map +0 -1
- package/dist/boss-phrases.d.ts +0 -9
- package/dist/boss-phrases.d.ts.map +0 -1
- package/dist/boss-phrases.js +0 -71
- package/dist/boss-phrases.js.map +0 -1
- package/dist/boss-store.d.ts +0 -245
- package/dist/boss-store.d.ts.map +0 -1
- package/dist/boss-store.js +0 -623
- package/dist/boss-store.js.map +0 -1
- package/dist/boss-system-prompt.d.ts +0 -3
- package/dist/boss-system-prompt.d.ts.map +0 -1
- package/dist/boss-system-prompt.js +0 -180
- package/dist/boss-system-prompt.js.map +0 -1
- package/dist/boss-tasks-overlay.d.ts +0 -22
- package/dist/boss-tasks-overlay.d.ts.map +0 -1
- package/dist/boss-tasks-overlay.js +0 -157
- package/dist/boss-tasks-overlay.js.map +0 -1
- package/dist/branding.d.ts +0 -32
- package/dist/branding.d.ts.map +0 -1
- package/dist/branding.js +0 -59
- package/dist/branding.js.map +0 -1
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.smoke.test.d.ts +0 -2
- package/dist/cli.smoke.test.d.ts.map +0 -1
- package/dist/cli.smoke.test.js +0 -48
- package/dist/cli.smoke.test.js.map +0 -1
- package/dist/colors.d.ts +0 -14
- package/dist/colors.d.ts.map +0 -1
- package/dist/colors.js +0 -31
- package/dist/colors.js.map +0 -1
- package/dist/discover.d.ts +0 -13
- package/dist/discover.d.ts.map +0 -1
- package/dist/discover.js +0 -92
- package/dist/discover.js.map +0 -1
- package/dist/event-queue.d.ts +0 -16
- package/dist/event-queue.d.ts.map +0 -1
- package/dist/event-queue.js +0 -39
- package/dist/event-queue.js.map +0 -1
- package/dist/index.d.ts +0 -6
- package/dist/index.d.ts.map +0 -1
- package/dist/link-command.d.ts +0 -2
- package/dist/link-command.d.ts.map +0 -1
- package/dist/link-command.js +0 -120
- package/dist/link-command.js.map +0 -1
- package/dist/links.d.ts +0 -11
- package/dist/links.d.ts.map +0 -1
- package/dist/links.js +0 -22
- package/dist/links.js.map +0 -1
- package/dist/logger.d.ts +0 -41
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -112
- package/dist/logger.js.map +0 -1
- package/dist/orchestrator-app.d.ts +0 -15
- package/dist/orchestrator-app.d.ts.map +0 -1
- package/dist/orchestrator-app.js +0 -599
- package/dist/orchestrator-app.js.map +0 -1
- package/dist/orchestrator.d.ts +0 -147
- package/dist/orchestrator.d.ts.map +0 -1
- package/dist/orchestrator.js +0 -707
- package/dist/orchestrator.js.map +0 -1
- package/dist/orchestrator.test.d.ts +0 -2
- package/dist/orchestrator.test.d.ts.map +0 -1
- package/dist/orchestrator.test.js +0 -55
- package/dist/orchestrator.test.js.map +0 -1
- package/dist/pixel-WB6VRJWP.js +0 -13
- package/dist/radio-picker.d.ts +0 -20
- package/dist/radio-picker.d.ts.map +0 -1
- package/dist/radio-picker.js +0 -31
- package/dist/radio-picker.js.map +0 -1
- package/dist/radio.d.ts +0 -43
- package/dist/radio.d.ts.map +0 -1
- package/dist/radio.js +0 -150
- package/dist/radio.js.map +0 -1
- package/dist/sessions.d.ts +0 -21
- package/dist/sessions.d.ts.map +0 -1
- package/dist/sessions.js +0 -122
- package/dist/sessions.js.map +0 -1
- package/dist/settings.d.ts +0 -11
- package/dist/settings.d.ts.map +0 -1
- package/dist/settings.js +0 -38
- package/dist/settings.js.map +0 -1
- package/dist/slash-commands.d.ts +0 -19
- package/dist/slash-commands.d.ts.map +0 -1
- package/dist/slash-commands.js +0 -76
- package/dist/slash-commands.js.map +0 -1
- package/dist/splash.d.ts +0 -21
- package/dist/splash.d.ts.map +0 -1
- package/dist/splash.js +0 -137
- package/dist/splash.js.map +0 -1
- package/dist/task-tools.d.ts +0 -18
- package/dist/task-tools.d.ts.map +0 -1
- package/dist/task-tools.js +0 -172
- package/dist/task-tools.js.map +0 -1
- package/dist/tasks-store.d.ts +0 -66
- package/dist/tasks-store.d.ts.map +0 -1
- package/dist/tasks-store.js +0 -199
- package/dist/tasks-store.js.map +0 -1
- package/dist/tasks-store.test.d.ts +0 -2
- package/dist/tasks-store.test.d.ts.map +0 -1
- package/dist/tasks-store.test.js +0 -138
- package/dist/tasks-store.test.js.map +0 -1
- package/dist/tool-formatters.d.ts +0 -7
- package/dist/tool-formatters.d.ts.map +0 -1
- package/dist/tool-formatters.js +0 -111
- package/dist/tool-formatters.js.map +0 -1
- package/dist/tools.d.ts +0 -26
- package/dist/tools.d.ts.map +0 -1
- package/dist/tools.js +0 -133
- package/dist/tools.js.map +0 -1
- package/dist/types.d.ts +0 -32
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/worker.d.ts +0 -47
- package/dist/worker.d.ts.map +0 -1
- package/dist/worker.js +0 -123
- package/dist/worker.js.map +0 -1
- /package/dist/{pixel-WB6VRJWP.js.map → pixel-WPYTQADG.js.map} +0 -0
package/dist/orchestrator-app.js
DELETED
|
@@ -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
|