@bubblebrain-ai/bubble 0.0.8 → 0.0.9
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/agent/categories.d.ts +34 -0
- package/dist/agent/categories.js +98 -0
- package/dist/agent/profiles.d.ts +4 -0
- package/dist/agent/profiles.js +2 -3
- package/dist/agent/subagent-control.d.ts +5 -0
- package/dist/agent/subagent-control.js +4 -0
- package/dist/agent/subagent-lifecycle-reminder.d.ts +3 -0
- package/dist/agent/subagent-lifecycle-reminder.js +102 -0
- package/dist/agent/subagent-route-format.d.ts +8 -0
- package/dist/agent/subagent-route-format.js +18 -0
- package/dist/agent/subtask-policy.d.ts +0 -1
- package/dist/agent/subtask-policy.js +0 -4
- package/dist/agent.d.ts +12 -0
- package/dist/agent.js +152 -13
- package/dist/config.d.ts +23 -3
- package/dist/config.js +59 -6
- package/dist/context/budget.d.ts +3 -3
- package/dist/context/budget.js +29 -15
- package/dist/context/compact.d.ts +23 -0
- package/dist/context/compact.js +129 -0
- package/dist/context/llm-compactor.d.ts +19 -0
- package/dist/context/llm-compactor.js +200 -0
- package/dist/context/projector.js +28 -12
- package/dist/context/token-estimator.d.ts +14 -0
- package/dist/context/token-estimator.js +106 -0
- package/dist/context/tool-output-truncate.d.ts +8 -0
- package/dist/context/tool-output-truncate.js +59 -0
- package/dist/context/usage.js +9 -9
- package/dist/main.js +43 -6
- package/dist/model-catalog.d.ts +9 -0
- package/dist/model-catalog.js +16 -0
- package/dist/orchestrator/default-hooks.js +18 -0
- package/dist/provider-openai-codex.d.ts +13 -2
- package/dist/provider-openai-codex.js +81 -32
- package/dist/provider-registry.js +20 -4
- package/dist/slash-commands/commands.js +24 -0
- package/dist/slash-commands/types.d.ts +7 -0
- package/dist/tools/agent-lifecycle.js +22 -4
- package/dist/tools/edit.js +2 -2
- package/dist/tools/glob.js +2 -1
- package/dist/tools/grep.js +2 -2
- package/dist/tools/lsp.js +2 -2
- package/dist/tools/path-utils.d.ts +2 -0
- package/dist/tools/path-utils.js +16 -0
- package/dist/tools/read.js +117 -5
- package/dist/tools/write.js +3 -2
- package/dist/tui-ink/app.d.ts +11 -2
- package/dist/tui-ink/app.js +191 -78
- package/dist/tui-ink/approval/approval-dialog.js +4 -1
- package/dist/tui-ink/approval/diff-view.js +2 -1
- package/dist/tui-ink/approval/select.js +2 -1
- package/dist/tui-ink/code-highlight.d.ts +2 -0
- package/dist/tui-ink/code-highlight.js +30 -2
- package/dist/tui-ink/detect-theme.d.ts +19 -0
- package/dist/tui-ink/detect-theme.js +123 -0
- package/dist/tui-ink/footer.js +4 -3
- package/dist/tui-ink/input-box.js +83 -26
- package/dist/tui-ink/input-history.d.ts +16 -0
- package/dist/tui-ink/input-history.js +81 -0
- package/dist/tui-ink/markdown.js +30 -20
- package/dist/tui-ink/message-list.js +112 -16
- package/dist/tui-ink/model-picker.js +6 -1
- package/dist/tui-ink/plan-confirm.js +2 -1
- package/dist/tui-ink/question-dialog.js +2 -1
- package/dist/tui-ink/run.d.ts +5 -1
- package/dist/tui-ink/run.js +30 -2
- package/dist/tui-ink/theme.d.ts +64 -35
- package/dist/tui-ink/theme.js +81 -8
- package/dist/tui-ink/todos.js +5 -3
- package/dist/tui-ink/trace-groups.d.ts +3 -1
- package/dist/tui-ink/trace-groups.js +93 -14
- package/dist/tui-ink/welcome.js +23 -4
- package/dist/types.d.ts +6 -0
- package/package.json +2 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { Box, Static, Text } from "ink";
|
|
4
|
-
import {
|
|
4
|
+
import { useTheme } from "./theme.js";
|
|
5
5
|
import { highlightCode, inferLang } from "./code-highlight.js";
|
|
6
6
|
import { MarkdownContent } from "./markdown.js";
|
|
7
7
|
import { buildTraceGroups, formatElapsed, formatTracePath, traceGroupLabel } from "./trace-groups.js";
|
|
8
8
|
import { EDIT_COLLAPSED_DIFF_LINES, formatEditSuccessSummary, getEditDiffDetails } from "./edit-diff.js";
|
|
9
|
+
import { formatSubagentRoute } from "../agent/subagent-route-format.js";
|
|
9
10
|
export function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, verboseTrace, pendingApproval, nowTick, welcomeBanner, }) {
|
|
10
11
|
const hasStreaming = !!(streamingContent ||
|
|
11
12
|
streamingReasoning ||
|
|
@@ -39,6 +40,7 @@ export function MessageList({ messages, streamingContent, streamingReasoning, st
|
|
|
39
40
|
} }), hasStreaming && (_jsx(StreamingMessage, { content: streamingContent, reasoning: streamingReasoning, tools: streamingTools, parts: streamingParts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: pendingApproval, nowTick: nowTick }))] }));
|
|
40
41
|
}
|
|
41
42
|
function MessageItem({ message, terminalColumns, verboseTrace, showExpandHint, nowTick, }) {
|
|
43
|
+
const theme = useTheme();
|
|
42
44
|
if (message.role === "user") {
|
|
43
45
|
return _jsx(UserMessageBlock, { content: message.content, terminalColumns: terminalColumns });
|
|
44
46
|
}
|
|
@@ -72,6 +74,7 @@ function MessageParts({ parts, terminalColumns, verboseTrace, pendingApproval, s
|
|
|
72
74
|
}) }));
|
|
73
75
|
}
|
|
74
76
|
function TimelineText({ content, compactTop, terminalColumns, }) {
|
|
77
|
+
const theme = useTheme();
|
|
75
78
|
if (!content.trim())
|
|
76
79
|
return null;
|
|
77
80
|
// marginLeft (2) + "⛬ " glyph (3 visual cells) = 5 cells consumed by the
|
|
@@ -115,6 +118,7 @@ function TraceGroupList({ toolCalls, terminalColumns, pendingApproval, nowTick,
|
|
|
115
118
|
return (_jsxs(Box, { flexDirection: "column", children: [activeGroup && (_jsx(TraceActivityLine, { group: activeGroup, pendingApproval: pendingApproval, nowTick: nowTick, terminalColumns: terminalColumns })), groups.map((group, idx) => (_jsx(TraceGroupBlock, { group: group, terminalColumns: terminalColumns, pendingApproval: pendingApproval, compactTop: idx === 0 && compactTop, nowTick: nowTick }, group.raw.map((tool) => tool.id).join(":"))))] }));
|
|
116
119
|
}
|
|
117
120
|
function TraceActivityLine({ group, pendingApproval, nowTick, terminalColumns, }) {
|
|
121
|
+
const theme = useTheme();
|
|
118
122
|
const waiting = isTraceGroupWaitingForApproval(group, pendingApproval);
|
|
119
123
|
const elapsed = formatElapsed(group.startedAt, nowTick);
|
|
120
124
|
const labelWidth = Math.max(20, terminalColumns - 26);
|
|
@@ -122,34 +126,39 @@ function TraceActivityLine({ group, pendingApproval, nowTick, terminalColumns, }
|
|
|
122
126
|
return (_jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { color: waiting ? theme.warning : theme.tracePending, children: "\u25CF " }), _jsxs(Text, { color: theme.traceDetail, children: [waiting ? "Waiting for approval" : "Working on", " "] }), _jsx(Text, { color: theme.traceAction, children: label }), elapsed && _jsxs(Text, { color: theme.traceDetail, children: [" \u00B7 ", elapsed] })] }));
|
|
123
127
|
}
|
|
124
128
|
function TraceGroupBlock({ group, terminalColumns, pendingApproval, compactTop, nowTick, }) {
|
|
129
|
+
const theme = useTheme();
|
|
125
130
|
const waiting = isTraceGroupWaitingForApproval(group, pendingApproval);
|
|
126
|
-
const status = traceGroupStatus(group, waiting, nowTick);
|
|
131
|
+
const status = traceGroupStatus(group, waiting, theme, nowTick);
|
|
127
132
|
const editTool = group.kind === "edit" && group.raw.length === 1 ? group.raw[0] : undefined;
|
|
128
133
|
const editDetails = editTool && !group.pending && !group.hasError ? getEditDiffDetails(editTool) : null;
|
|
129
134
|
if (editTool && editDetails) {
|
|
130
135
|
return (_jsx(EditTraceBlock, { tool: editTool, details: editDetails, terminalColumns: terminalColumns, compactTop: compactTop, status: status }));
|
|
131
136
|
}
|
|
132
|
-
const
|
|
133
|
-
const
|
|
137
|
+
const allErrored = group.hasError && group.errorCount >= group.raw.length && !group.pending;
|
|
138
|
+
const titleColor = allErrored ? theme.error : theme.traceAction;
|
|
139
|
+
const detailColor = allErrored ? theme.error : theme.traceDetail;
|
|
134
140
|
const commandWidth = Math.max(14, terminalColumns - group.title.length - 16);
|
|
135
141
|
const detailWidth = Math.max(20, terminalColumns - 8);
|
|
136
142
|
const detailLines = group.previewLines.length > 0 ? group.previewLines : group.items;
|
|
137
|
-
return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: titleColor, children: group.title }), group.command ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: theme.traceCommand, children: truncateVisual(group.command, commandWidth) })] })) : group.count !== undefined && group.noun ? (_jsxs(Text, { color: theme.traceCount, children: [" ", group.count, " ", group.noun] })) : null, status && _jsxs(Text, { color: status.color, children: [" ", status.text] })] }), detailLines.length > 0 && (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: detailLines.map((line, index) => (_jsxs(Box, { marginLeft: index === 0 ? 0 : 2, children: [index === 0 && _jsx(Text, { color: theme.traceDetail, children: "\u21B3 " }), _jsx(Text, { color: detailColor, children: truncateVisual(line, detailWidth - (index === 0 ? 2 : 0)) })] }, index))) })), group.omitted > 0 && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: theme.traceDetail, children: ["... ", group.omitted, " more, Ctrl+O to view"] }) }))] }));
|
|
143
|
+
return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: titleColor, children: group.title }), group.command ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: theme.traceCommand, children: truncateVisual(group.command, commandWidth) })] })) : group.count !== undefined && group.noun ? (_jsxs(Text, { color: theme.traceCount, children: [" ", group.count, " ", group.noun] })) : null, status && _jsxs(Text, { color: status.color, children: [" ", status.text] })] }), detailLines.length > 0 && (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: detailLines.map((line, index) => (_jsxs(Box, { marginLeft: index === 0 ? 0 : 2, children: [index === 0 && _jsx(Text, { color: theme.traceDetail, children: "\u21B3 " }), _jsx(Text, { color: detailColor, children: truncateVisual(line, detailWidth - (index === 0 ? 2 : 0)) })] }, index))) })), group.errorLines.length > 0 && (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: group.errorLines.map((line, index) => (_jsxs(Box, { marginLeft: index === 0 ? 0 : 2, children: [index === 0 && _jsx(Text, { color: theme.traceDetail, children: "\u21B3 " }), _jsx(Text, { color: theme.error, children: truncateVisual(line, detailWidth - (index === 0 ? 2 : 0)) })] }, `error-${index}`))) })), group.omitted > 0 && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: theme.traceDetail, children: ["... ", group.omitted, " more, Ctrl+O to view"] }) }))] }));
|
|
138
144
|
}
|
|
139
145
|
function EditTraceBlock({ tool, details, terminalColumns, compactTop, status, }) {
|
|
146
|
+
const theme = useTheme();
|
|
140
147
|
const path = formatTracePath(details.path ?? tool.args.path ?? "");
|
|
141
148
|
const pathWidth = Math.max(14, terminalColumns - 12);
|
|
142
149
|
return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.traceAction, children: "Edit" }), path && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: theme.traceCommand, children: truncateVisual(path, pathWidth) })] })), status && _jsxs(Text, { color: status.color, children: [" ", status.text] })] }), _jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { color: theme.traceDetail, children: "\u23BF " }), _jsx(Text, { color: theme.success, children: formatEditSuccessSummary(details) })] }), _jsx(DiffBlock, { diff: details.diff, terminalColumns: terminalColumns, maxLines: EDIT_COLLAPSED_DIFF_LINES, verbose: false, showExpandHint: true })] }));
|
|
143
150
|
}
|
|
144
|
-
function traceGroupStatus(group, waitingApproval, nowTick) {
|
|
151
|
+
function traceGroupStatus(group, waitingApproval, theme, nowTick) {
|
|
145
152
|
if (waitingApproval)
|
|
146
153
|
return { text: "waiting for approval", color: theme.warning };
|
|
147
154
|
if (group.pending) {
|
|
148
155
|
const elapsed = formatElapsed(group.startedAt, nowTick);
|
|
149
156
|
return { text: elapsed ? `running · ${elapsed}` : "running", color: theme.tracePending };
|
|
150
157
|
}
|
|
151
|
-
if (group.hasError)
|
|
152
|
-
|
|
158
|
+
if (group.hasError) {
|
|
159
|
+
const count = group.errorCount || 1;
|
|
160
|
+
return { text: count === 1 ? "1 error" : `${count} errors`, color: theme.error };
|
|
161
|
+
}
|
|
153
162
|
return null;
|
|
154
163
|
}
|
|
155
164
|
function findActiveTraceGroup(groups, pendingApproval) {
|
|
@@ -173,16 +182,18 @@ function approvalMatchesTool(hint, tc) {
|
|
|
173
182
|
return !hint.path || hint.path === tc.args.path;
|
|
174
183
|
}
|
|
175
184
|
function ReasoningTraceBlock({ reasoning }) {
|
|
185
|
+
const theme = useTheme();
|
|
176
186
|
const lines = React.useMemo(() => reasoning.split("\n").filter((l) => l.trim() !== ""), [reasoning]);
|
|
177
187
|
return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: [_jsxs(Text, { color: theme.thinkingDim, dimColor: true, children: ["\u273B Reasoning trace", lines.length > 0 ? ` · ${lines.length} line${lines.length === 1 ? "" : "s"}` : ""] }), lines.map((line, i) => (_jsx(Text, { color: theme.thinkingDim, dimColor: true, italic: true, children: line }, i)))] }));
|
|
178
188
|
}
|
|
179
189
|
function UserMessageBlock({ content, terminalColumns }) {
|
|
180
|
-
const
|
|
181
|
-
|
|
190
|
+
const theme = useTheme();
|
|
191
|
+
// Rail (▌ + space) takes 2 cols; reserve 2 cols inside the fill for left/right gutters.
|
|
192
|
+
const horizontalRoom = Math.max(20, terminalColumns - 2);
|
|
193
|
+
const bubbleTextWidth = Math.max(1, horizontalRoom - 2);
|
|
182
194
|
const wrappedLines = content
|
|
183
195
|
.split("\n")
|
|
184
|
-
.flatMap((line) => wrapByVisualWidth(line,
|
|
185
|
-
const bubbleTextWidth = Math.min(maxBubbleTextWidth, Math.max(8, ...wrappedLines.map((line) => visualWidth(line))));
|
|
196
|
+
.flatMap((line) => wrapByVisualWidth(line, bubbleTextWidth));
|
|
186
197
|
return (_jsx(Box, { flexDirection: "column", children: wrappedLines.map((line, index) => (_jsxs(Box, { children: [_jsx(Text, { color: theme.userRail, children: "\u258C " }), _jsx(Text, { backgroundColor: theme.userMessageBg, color: theme.userMessageText, children: ` ${padVisual(line || " ", bubbleTextWidth)} ` })] }, index))) }));
|
|
187
198
|
}
|
|
188
199
|
const TOOL_DISPLAY_NAMES = {
|
|
@@ -276,9 +287,72 @@ function summarizeToolResult(tc) {
|
|
|
276
287
|
return lineCount > 0 ? p(lineCount, "line", "lines") : "Done";
|
|
277
288
|
}
|
|
278
289
|
}
|
|
290
|
+
function subagentsFrom(toolCall) {
|
|
291
|
+
const raw = toolCall.metadata?.subagents;
|
|
292
|
+
if (!Array.isArray(raw))
|
|
293
|
+
return [];
|
|
294
|
+
return raw.filter((item) => typeof item === "object" && item !== null);
|
|
295
|
+
}
|
|
296
|
+
function latestSubagentNote(subagent) {
|
|
297
|
+
const note = subagent.error
|
|
298
|
+
|| subagent.toolNotes?.filter(Boolean).at(-1)
|
|
299
|
+
|| subagent.summary
|
|
300
|
+
|| subagent.task
|
|
301
|
+
|| "";
|
|
302
|
+
return note.replace(/\r\n/g, "\n").split("\n").map((line) => line.trim()).find(Boolean) ?? "";
|
|
303
|
+
}
|
|
304
|
+
function subagentLabel(subagent) {
|
|
305
|
+
return subagent.nickname ?? subagent.agentName ?? "subagent";
|
|
306
|
+
}
|
|
307
|
+
function subagentRole(subagent) {
|
|
308
|
+
return [subagent.agentName, subagent.category ? `/${subagent.category}` : ""].join("") || "default";
|
|
309
|
+
}
|
|
310
|
+
function subagentDescriptor(subagent, includeThinking = false) {
|
|
311
|
+
const route = formatSubagentRoute(subagent.route, { includeThinking });
|
|
312
|
+
const role = subagentRole(subagent);
|
|
313
|
+
return route ? `${role} @ ${route}` : role;
|
|
314
|
+
}
|
|
315
|
+
function subagentStatusColor(status, theme) {
|
|
316
|
+
if (status === "completed")
|
|
317
|
+
return theme.success;
|
|
318
|
+
if (status === "failed" || status === "blocked" || status === "cancelled")
|
|
319
|
+
return theme.error;
|
|
320
|
+
if (status === "queued")
|
|
321
|
+
return theme.muted;
|
|
322
|
+
return theme.toolPending;
|
|
323
|
+
}
|
|
324
|
+
function subagentSummary(subagents) {
|
|
325
|
+
if (subagents.length === 0)
|
|
326
|
+
return "no subagents";
|
|
327
|
+
const counts = new Map();
|
|
328
|
+
for (const subagent of subagents) {
|
|
329
|
+
const status = subagent.status ?? "running";
|
|
330
|
+
counts.set(status, (counts.get(status) ?? 0) + 1);
|
|
331
|
+
}
|
|
332
|
+
const order = ["running", "queued", "completed", "blocked", "failed", "cancelled"];
|
|
333
|
+
return order
|
|
334
|
+
.filter((status) => counts.has(status))
|
|
335
|
+
.map((status) => `${counts.get(status)} ${status}`)
|
|
336
|
+
.join(" ");
|
|
337
|
+
}
|
|
338
|
+
function sortSubagents(subagents) {
|
|
339
|
+
const rank = {
|
|
340
|
+
running: 0,
|
|
341
|
+
blocked: 1,
|
|
342
|
+
failed: 2,
|
|
343
|
+
queued: 3,
|
|
344
|
+
cancelled: 4,
|
|
345
|
+
completed: 5,
|
|
346
|
+
};
|
|
347
|
+
return [...subagents].sort((a, b) => (rank[a.status ?? "running"] ?? 9) - (rank[b.status ?? "running"] ?? 9));
|
|
348
|
+
}
|
|
279
349
|
const COLLAPSED_PREVIEW_LINES = 10;
|
|
280
350
|
const EXPANDED_PREVIEW_LINES = 50;
|
|
281
351
|
function ToolCallDisplay({ toolCall, isStreaming, verbose, terminalColumns, showExpandHint = false, waitingApproval = false, compactTop = false, nowTick, }) {
|
|
352
|
+
const theme = useTheme();
|
|
353
|
+
if (toolCall.metadata?.kind === "subagent") {
|
|
354
|
+
return (_jsx(SubagentToolDisplay, { toolCall: toolCall, verbose: verbose, terminalColumns: terminalColumns, compactTop: compactTop }));
|
|
355
|
+
}
|
|
282
356
|
// Show raw output immediately, then upgrade to highlighted ANSI when shiki
|
|
283
357
|
// resolves. Avoids a noticeable "flash" where the line jumps from empty/raw
|
|
284
358
|
// to colorized after a tick.
|
|
@@ -357,7 +431,25 @@ function ToolCallDisplay({ toolCall, isStreaming, verbose, terminalColumns, show
|
|
|
357
431
|
const isWritePreview = toolCall.name === "write" && !toolCall.isError && toolCall.result !== undefined;
|
|
358
432
|
return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: bulletColor, children: [glyph, " "] }), _jsx(Text, { bold: true, color: theme.toolName, children: name }), header && _jsxs(Text, { color: theme.muted, children: ["(", header, ")"] })] }), _jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: summaryColor, children: ["\u23BF ", summary] }) }), toolCall.isError && toolCall.result && (_jsx(Box, { marginLeft: 4, flexDirection: "column", children: toolCall.result.replace(/\r\n/g, "\n").split("\n").slice(0, 6).map((line, i) => (_jsx(Text, { color: theme.error, children: line }, i))) })), isEditDiff && (_jsx(DiffBlock, { diff: editDetails.diff, terminalColumns: terminalColumns, maxLines: maxLines, verbose: verbose, showExpandHint: showExpandHint })), isWritePreview && (_jsx(WritePreview, { content: String(toolCall.args.content || ""), maxLines: maxLines, verbose: verbose, showExpandHint: showExpandHint })), !toolCall.isError && !isEditDiff && !isWritePreview && highlighted && (_jsx(OutputPreview, { text: highlighted, maxLines: maxLines, verbose: verbose, showExpandHint: showExpandHint }))] }));
|
|
359
433
|
}
|
|
360
|
-
function
|
|
434
|
+
function SubagentToolDisplay({ toolCall, verbose, terminalColumns, compactTop, }) {
|
|
435
|
+
const theme = useTheme();
|
|
436
|
+
const subagents = subagentsFrom(toolCall);
|
|
437
|
+
const hasError = toolCall.isError || subagents.some((subagent) => (subagent.status === "failed" || subagent.status === "blocked" || subagent.status === "cancelled"));
|
|
438
|
+
const bulletColor = hasError ? theme.error : toolCall.result === undefined ? theme.toolPending : theme.user;
|
|
439
|
+
const detailWidth = Math.max(24, terminalColumns - 10);
|
|
440
|
+
const rows = verbose ? sortSubagents(subagents) : sortSubagents(subagents).slice(0, 4);
|
|
441
|
+
const omitted = Math.max(0, subagents.length - rows.length);
|
|
442
|
+
return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: bulletColor, children: "\u21B3 " }), _jsx(Text, { bold: true, color: theme.toolName, children: "Subagents" }), subagents.length > 0 && _jsxs(Text, { color: theme.muted, children: [" ", subagentSummary(subagents)] })] }), rows.length > 0 && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [rows.map((subagent, index) => {
|
|
443
|
+
const status = subagent.status ?? "running";
|
|
444
|
+
const label = padVisual(truncateVisual(subagentLabel(subagent), 10), 10);
|
|
445
|
+
const descriptorWidth = verbose ? 42 : 32;
|
|
446
|
+
const descriptor = padVisual(truncateVisual(subagentDescriptor(subagent), descriptorWidth), descriptorWidth);
|
|
447
|
+
const note = truncateVisual(latestSubagentNote(subagent), Math.max(12, detailWidth - 16 - descriptorWidth - 10));
|
|
448
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: subagentStatusColor(status, theme), children: label }), _jsxs(Text, { color: theme.traceAction, children: [" ", descriptor] }), _jsxs(Text, { color: subagentStatusColor(status, theme), children: [" ", padVisual(status, 9)] }), note && _jsxs(Text, { color: subagent.error ? theme.error : theme.traceDetail, children: [" ", note] })] }, subagent.subAgentId ?? `${subagentLabel(subagent)}-${index}`));
|
|
449
|
+
}), omitted > 0 && (_jsxs(Text, { color: theme.muted, children: ["... ", omitted, " more, Ctrl+O to view"] }))] })), subagents.length === 0 && toolCall.result && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: hasError ? theme.error : theme.muted, children: summarizeToolResult(toolCall) }) }))] }));
|
|
450
|
+
}
|
|
451
|
+
function TruncationHint({ remaining, verbose, showExpandHint, }) {
|
|
452
|
+
const theme = useTheme();
|
|
361
453
|
if (remaining <= 0)
|
|
362
454
|
return null;
|
|
363
455
|
const noun = `line${remaining === 1 ? "" : "s"}`;
|
|
@@ -367,19 +459,21 @@ function renderTruncationHint(remaining, verbose, showExpandHint) {
|
|
|
367
459
|
return (_jsxs(Text, { color: theme.muted, children: ["\u2026 +", remaining, " ", noun, showExpandHint ? " (ctrl+o to expand)" : ""] }));
|
|
368
460
|
}
|
|
369
461
|
function OutputPreview({ text, maxLines, verbose, showExpandHint, }) {
|
|
462
|
+
const theme = useTheme();
|
|
370
463
|
const lines = text.split("\n");
|
|
371
464
|
const shown = lines.slice(0, maxLines);
|
|
372
465
|
const remaining = Math.max(0, lines.length - maxLines);
|
|
373
466
|
if (shown.length === 0 || (shown.length === 1 && shown[0] === ""))
|
|
374
467
|
return null;
|
|
375
|
-
return (_jsxs(Box, { flexDirection: "column", marginLeft: 4, children: [shown.map((line, i) => (_jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: "\u2502 " }), _jsx(Text, { children: line })] }, i))),
|
|
468
|
+
return (_jsxs(Box, { flexDirection: "column", marginLeft: 4, children: [shown.map((line, i) => (_jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: "\u2502 " }), _jsx(Text, { children: line })] }, i))), _jsx(TruncationHint, { remaining: remaining, verbose: verbose, showExpandHint: showExpandHint })] }));
|
|
376
469
|
}
|
|
377
470
|
function WritePreview({ content, maxLines, verbose, showExpandHint, }) {
|
|
471
|
+
const theme = useTheme();
|
|
378
472
|
const lines = content.split("\n");
|
|
379
473
|
const shown = lines.slice(0, maxLines);
|
|
380
474
|
const remaining = Math.max(0, lines.length - maxLines);
|
|
381
475
|
const numWidth = Math.max(2, String(lines.length).length);
|
|
382
|
-
return (_jsxs(Box, { flexDirection: "column", marginLeft: 4, children: [shown.map((line, i) => (_jsxs(Box, { children: [_jsxs(Text, { color: theme.muted, children: [String(i + 1).padStart(numWidth, " "), " "] }), _jsx(Text, { children: line })] }, i))),
|
|
476
|
+
return (_jsxs(Box, { flexDirection: "column", marginLeft: 4, children: [shown.map((line, i) => (_jsxs(Box, { children: [_jsxs(Text, { color: theme.muted, children: [String(i + 1).padStart(numWidth, " "), " "] }), _jsx(Text, { children: line })] }, i))), _jsx(TruncationHint, { remaining: remaining, verbose: verbose, showExpandHint: showExpandHint })] }));
|
|
383
477
|
}
|
|
384
478
|
function parseDiffLines(body) {
|
|
385
479
|
const result = [];
|
|
@@ -420,6 +514,7 @@ function parseDiffLines(body) {
|
|
|
420
514
|
return result;
|
|
421
515
|
}
|
|
422
516
|
function DiffBlock({ diff, terminalColumns, maxLines, verbose, showExpandHint, }) {
|
|
517
|
+
const theme = useTheme();
|
|
423
518
|
const lines = parseDiffLines(diff);
|
|
424
519
|
const shown = lines.slice(0, maxLines);
|
|
425
520
|
const remaining = Math.max(0, lines.length - maxLines);
|
|
@@ -446,13 +541,14 @@ function DiffBlock({ diff, terminalColumns, maxLines, verbose, showExpandHint, }
|
|
|
446
541
|
const padded = padVisual(truncated, contentWidth);
|
|
447
542
|
const lineText = ` ${numStr} ${sign} ${padded}`;
|
|
448
543
|
return (_jsx(Text, { backgroundColor: bg, color: theme.userMessageText, children: lineText }, i));
|
|
449
|
-
}),
|
|
544
|
+
}), _jsx(TruncationHint, { remaining: remaining, verbose: verbose, showExpandHint: showExpandHint })] }));
|
|
450
545
|
}
|
|
451
546
|
/**
|
|
452
547
|
* "Edited 3 files (+42 -8) — a.ts, b.ts" digest below the assistant turn.
|
|
453
548
|
* Surfaces only when there is at least one file-mutating tool call.
|
|
454
549
|
*/
|
|
455
550
|
function TurnDigest({ toolCalls }) {
|
|
551
|
+
const theme = useTheme();
|
|
456
552
|
const digest = React.useMemo(() => buildDigest(toolCalls), [toolCalls]);
|
|
457
553
|
if (!digest)
|
|
458
554
|
return null;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect, useMemo } from "react";
|
|
3
3
|
import { Box, Text, useInput, usePaste, useStdout } from "ink";
|
|
4
|
-
import {
|
|
4
|
+
import { useTheme } from "./theme.js";
|
|
5
5
|
import { encodeModel, decodeModel, displayModel, isUserVisibleProvider } from "../provider-registry.js";
|
|
6
6
|
import { listBuiltinModels } from "../model-catalog.js";
|
|
7
7
|
export function ModelPicker({ registry, current, recent, onSelect, onCancel }) {
|
|
8
|
+
const theme = useTheme();
|
|
8
9
|
const { stdout } = useStdout();
|
|
9
10
|
const termHeight = stdout?.rows || 24;
|
|
10
11
|
const maxVisible = Math.max(5, termHeight - 10);
|
|
@@ -122,6 +123,7 @@ export function ModelPicker({ registry, current, recent, onSelect, onCancel }) {
|
|
|
122
123
|
})] })] }));
|
|
123
124
|
}
|
|
124
125
|
function SearchField({ query, placeholder }) {
|
|
126
|
+
const theme = useTheme();
|
|
125
127
|
const [cursorVisible, setCursorVisible] = useState(true);
|
|
126
128
|
useEffect(() => {
|
|
127
129
|
const t = setInterval(() => setCursorVisible((v) => !v), 500);
|
|
@@ -189,6 +191,7 @@ function preferredModelIndex(options, current) {
|
|
|
189
191
|
return idx >= 0 ? idx : 0;
|
|
190
192
|
}
|
|
191
193
|
export function ProviderPicker({ providers, current, onSelect, onCancel, title }) {
|
|
194
|
+
const theme = useTheme();
|
|
192
195
|
const { stdout } = useStdout();
|
|
193
196
|
const termHeight = stdout?.rows || 24;
|
|
194
197
|
const maxVisible = Math.max(5, termHeight - 8);
|
|
@@ -240,6 +243,7 @@ export function ProviderPicker({ providers, current, onSelect, onCancel, title }
|
|
|
240
243
|
}) })] }));
|
|
241
244
|
}
|
|
242
245
|
export function KeyPicker({ providerName, onSubmit, onCancel }) {
|
|
246
|
+
const theme = useTheme();
|
|
243
247
|
const [value, setValue] = useState("");
|
|
244
248
|
useInput((input, key) => {
|
|
245
249
|
if (key.escape) {
|
|
@@ -270,6 +274,7 @@ export function KeyPicker({ providerName, onSubmit, onCancel }) {
|
|
|
270
274
|
return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsxs(Text, { bold: true, color: theme.accent, children: ["Enter API Key for ", providerName] }), _jsx(Text, { color: theme.muted, children: "Paste or type the key \u00B7 Enter to submit \u00B7 Esc to cancel" }), _jsx(SearchField, { query: value.replace(/./g, "*"), placeholder: "Paste your key here..." })] }));
|
|
271
275
|
}
|
|
272
276
|
export function SkillPicker({ skills, onSelect, onCancel }) {
|
|
277
|
+
const theme = useTheme();
|
|
273
278
|
const { stdout } = useStdout();
|
|
274
279
|
const termHeight = stdout?.rows || 24;
|
|
275
280
|
const maxVisible = Math.max(5, termHeight - 8);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from "react";
|
|
3
3
|
import { Box, Text, useInput } from "ink";
|
|
4
|
-
import {
|
|
4
|
+
import { useTheme } from "./theme.js";
|
|
5
5
|
import { MarkdownContent } from "./markdown.js";
|
|
6
6
|
export function PlanConfirm({ initialPlan, onApprove, onReject }) {
|
|
7
|
+
const theme = useTheme();
|
|
7
8
|
const [stage, setStage] = useState("view");
|
|
8
9
|
const [draft, setDraft] = useState(initialPlan);
|
|
9
10
|
const [cursor, setCursor] = useState(initialPlan.length);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo, useState } from "react";
|
|
3
3
|
import { Box, Text, useInput } from "ink";
|
|
4
|
-
import {
|
|
4
|
+
import { useTheme } from "./theme.js";
|
|
5
5
|
export function QuestionDialog({ request, onSubmit, onCancel }) {
|
|
6
|
+
const theme = useTheme();
|
|
6
7
|
const [index, setIndex] = useState(0);
|
|
7
8
|
const [selected, setSelected] = useState(0);
|
|
8
9
|
const [custom, setCustom] = useState("");
|
package/dist/tui-ink/run.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type { McpManager } from "../mcp/manager.js";
|
|
|
11
11
|
import type { LspService } from "../lsp/index.js";
|
|
12
12
|
import type { QuestionController } from "../question/index.js";
|
|
13
13
|
import type { MemoryScope } from "../memory/index.js";
|
|
14
|
+
import type { ResolvedTheme, ThemeMode } from "./theme.js";
|
|
14
15
|
export interface RunTuiOptions {
|
|
15
16
|
sessionManager?: SessionManager;
|
|
16
17
|
createProvider?: (providerId: string, apiKey: string, baseURL: string) => Provider;
|
|
@@ -23,7 +24,10 @@ export interface RunTuiOptions {
|
|
|
23
24
|
settingsManager?: SettingsManager;
|
|
24
25
|
lspService?: LspService;
|
|
25
26
|
mcpManager?: McpManager;
|
|
26
|
-
|
|
27
|
+
themeMode?: ThemeMode;
|
|
28
|
+
themeOverrides?: Record<string, string>;
|
|
29
|
+
detectedTheme?: ResolvedTheme;
|
|
30
|
+
onThemeModeChange?: (mode: ThemeMode) => void;
|
|
27
31
|
flushMemory?: () => Promise<void>;
|
|
28
32
|
runMemoryCompaction?: () => Promise<string>;
|
|
29
33
|
runMemorySummary?: (scope?: MemoryScope) => Promise<string>;
|
package/dist/tui-ink/run.js
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { render } from "ink";
|
|
3
|
+
import chalk from "chalk";
|
|
3
4
|
import { App } from "./app.js";
|
|
5
|
+
import { warmHighlighter } from "./code-highlight.js";
|
|
4
6
|
export async function runTui(agent, args, options = {}) {
|
|
5
|
-
|
|
7
|
+
// Kick off shiki load before the first code block reaches <Static>. Fire and
|
|
8
|
+
// forget — CodeBlock's lazy init falls back to raw lines if this isn't ready
|
|
9
|
+
// yet, so callers don't need to await it.
|
|
10
|
+
warmHighlighter();
|
|
11
|
+
let exitSummary;
|
|
12
|
+
const instance = render(_jsx(App, { agent: agent, args: args, sessionManager: options.sessionManager, createProvider: options.createProvider, registry: options.registry, skillRegistry: options.skillRegistry, planHandlerRef: options.planHandlerRef, approvalHandlerRef: options.approvalHandlerRef, questionController: options.questionController, bashAllowlist: options.bashAllowlist, settingsManager: options.settingsManager, lspService: options.lspService, mcpManager: options.mcpManager, themeMode: options.themeMode, themeOverrides: options.themeOverrides, detectedTheme: options.detectedTheme, onThemeModeChange: options.onThemeModeChange, flushMemory: options.flushMemory, runMemoryCompaction: options.runMemoryCompaction, runMemorySummary: options.runMemorySummary, runMemoryRefresh: options.runMemoryRefresh, bypassEnabled: options.bypassEnabled, onExit: (summary) => {
|
|
6
13
|
// The app already called useApp().exit() inside requestExit, which
|
|
7
14
|
// triggers Ink's own unmount + TTY restore. waitUntilExit() below is
|
|
8
15
|
// the canonical signal that we're done — we deliberately do *not*
|
|
9
16
|
// call instance.unmount() again here to avoid double-teardown
|
|
10
|
-
// warnings on React 19.
|
|
17
|
+
// warnings on React 19. We capture the summary and render it after
|
|
18
|
+
// teardown so it lands in the real shell scrollback (Claude-Code style).
|
|
19
|
+
exitSummary = summary;
|
|
11
20
|
} }), {
|
|
12
21
|
kittyKeyboard: {
|
|
13
22
|
mode: "enabled",
|
|
@@ -21,5 +30,24 @@ export async function runTui(agent, args, options = {}) {
|
|
|
21
30
|
// the cursor to column 0 before handing control back to the shell.
|
|
22
31
|
if (process.stdout.isTTY) {
|
|
23
32
|
process.stdout.write("\n");
|
|
33
|
+
if (exitSummary) {
|
|
34
|
+
process.stdout.write(formatExitSummary(exitSummary) + "\n");
|
|
35
|
+
}
|
|
24
36
|
}
|
|
25
37
|
}
|
|
38
|
+
function formatExitSummary(summary) {
|
|
39
|
+
const label = "Total duration (wall):";
|
|
40
|
+
return chalk.dim(`${label} ${formatWallMs(summary.wallMs)}`);
|
|
41
|
+
}
|
|
42
|
+
function formatWallMs(ms) {
|
|
43
|
+
const totalSeconds = Math.max(0, Math.round(ms / 1000));
|
|
44
|
+
if (totalSeconds < 60)
|
|
45
|
+
return `${totalSeconds}s`;
|
|
46
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
47
|
+
const seconds = totalSeconds % 60;
|
|
48
|
+
if (minutes < 60)
|
|
49
|
+
return `${minutes}m ${seconds}s`;
|
|
50
|
+
const hours = Math.floor(minutes / 60);
|
|
51
|
+
const minutesRest = minutes % 60;
|
|
52
|
+
return `${hours}h ${minutesRest}m ${seconds}s`;
|
|
53
|
+
}
|
package/dist/tui-ink/theme.d.ts
CHANGED
|
@@ -1,37 +1,66 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Color themes for the TUI.
|
|
3
|
+
*
|
|
4
|
+
* Two base palettes are shipped: `darkTheme` for dark terminal backgrounds and
|
|
5
|
+
* `lightTheme` for light ones. The shape is identical so consumers depend on
|
|
6
|
+
* `Theme` rather than either palette directly. Active palette is provided
|
|
7
|
+
* through `ThemeContext` so components re-render automatically when the
|
|
8
|
+
* user switches via `/theme` at runtime.
|
|
3
9
|
*/
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
10
|
+
export type ResolvedTheme = "light" | "dark";
|
|
11
|
+
export type ThemeMode = "auto" | ResolvedTheme;
|
|
12
|
+
export interface Theme {
|
|
13
|
+
user: string;
|
|
14
|
+
agent: string;
|
|
15
|
+
error: string;
|
|
16
|
+
warning: string;
|
|
17
|
+
success: string;
|
|
18
|
+
accent: string;
|
|
19
|
+
border: string;
|
|
20
|
+
borderActive: string;
|
|
21
|
+
inputBorder: string;
|
|
22
|
+
inputBorderDisabled: string;
|
|
23
|
+
inputBg: string;
|
|
24
|
+
inputBgDisabled: string;
|
|
25
|
+
inputText: string;
|
|
26
|
+
inputPlaceholder: string;
|
|
27
|
+
muted: string;
|
|
28
|
+
dim: string;
|
|
29
|
+
thinking: string;
|
|
30
|
+
thinkingDim: string;
|
|
31
|
+
toolName: string;
|
|
32
|
+
toolResult: string;
|
|
33
|
+
toolError: string;
|
|
34
|
+
toolPending: string;
|
|
35
|
+
code: string;
|
|
36
|
+
traceAction: string;
|
|
37
|
+
traceCount: string;
|
|
38
|
+
traceDetail: string;
|
|
39
|
+
traceCommand: string;
|
|
40
|
+
tracePending: string;
|
|
41
|
+
userMessageBorder: string;
|
|
42
|
+
userMessageBg: string;
|
|
43
|
+
userMessageText: string;
|
|
44
|
+
userRail: string;
|
|
45
|
+
diffAdd: string;
|
|
46
|
+
diffRemove: string;
|
|
47
|
+
diffAddFg: string;
|
|
48
|
+
diffRemoveFg: string;
|
|
49
|
+
}
|
|
50
|
+
export declare const darkTheme: Theme;
|
|
51
|
+
/**
|
|
52
|
+
* Light palette. Two ground rules drove the color choices:
|
|
53
|
+
* 1. Named ANSI colors that render OK on both backgrounds (red/green/blue)
|
|
54
|
+
* are kept by name so the user's terminal palette overrides remain
|
|
55
|
+
* effective.
|
|
56
|
+
* 2. Specific hex values are used wherever the dark palette assumed a dark
|
|
57
|
+
* background (notably accent/code/trace colors and message bubbles).
|
|
58
|
+
* Each hex was picked to clear WCAG AA contrast (4.5:1) against a near-
|
|
59
|
+
* white background (#fafafa) or, when applicable, against the explicit
|
|
60
|
+
* surface color in the same palette (e.g. diffAddFg vs diffAdd).
|
|
61
|
+
*/
|
|
62
|
+
export declare const lightTheme: Theme;
|
|
63
|
+
export declare const ThemeProvider: import("react").Provider<Theme>;
|
|
64
|
+
export declare function useTheme(): Theme;
|
|
65
|
+
/** Build the active palette given a resolved mode and optional overrides. */
|
|
66
|
+
export declare function paletteFor(mode: ResolvedTheme, overrides?: Record<string, string>): Theme;
|
package/dist/tui-ink/theme.js
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Color themes for the TUI.
|
|
3
|
+
*
|
|
4
|
+
* Two base palettes are shipped: `darkTheme` for dark terminal backgrounds and
|
|
5
|
+
* `lightTheme` for light ones. The shape is identical so consumers depend on
|
|
6
|
+
* `Theme` rather than either palette directly. Active palette is provided
|
|
7
|
+
* through `ThemeContext` so components re-render automatically when the
|
|
8
|
+
* user switches via `/theme` at runtime.
|
|
3
9
|
*/
|
|
4
|
-
|
|
5
|
-
|
|
10
|
+
import { createContext, useContext } from "react";
|
|
11
|
+
export const darkTheme = {
|
|
6
12
|
user: "green",
|
|
7
13
|
agent: "blue",
|
|
8
14
|
error: "red",
|
|
9
15
|
warning: "yellow",
|
|
10
16
|
success: "green",
|
|
11
|
-
// UI chrome
|
|
12
17
|
accent: "cyan",
|
|
13
18
|
border: "gray",
|
|
14
19
|
borderActive: "cyan",
|
|
15
20
|
inputBorder: "#8A7FC6",
|
|
21
|
+
inputBorderDisabled: "#4a4754",
|
|
22
|
+
inputBg: "#1c1c24",
|
|
23
|
+
inputBgDisabled: "#161620",
|
|
24
|
+
inputText: "#f3f3f7",
|
|
25
|
+
inputPlaceholder: "#6c6a78",
|
|
16
26
|
muted: "gray",
|
|
17
27
|
dim: "gray",
|
|
18
|
-
// Content
|
|
19
28
|
thinking: "magenta",
|
|
20
29
|
thinkingDim: "gray",
|
|
21
30
|
toolName: "cyan",
|
|
@@ -28,15 +37,79 @@ export const theme = {
|
|
|
28
37
|
traceDetail: "gray",
|
|
29
38
|
traceCommand: "#59BCE8",
|
|
30
39
|
tracePending: "yellow",
|
|
31
|
-
// Message surfaces — user input uses a subtle fill plus a left rail so it is
|
|
32
|
-
// visually separate from assistant/tool trace output without becoming noisy.
|
|
33
40
|
userMessageBorder: "#8A7FC6",
|
|
34
41
|
userMessageBg: "#2a2a34",
|
|
35
42
|
userMessageText: "#f3f3f7",
|
|
36
43
|
userRail: "#8A7FC6",
|
|
37
|
-
// Diff
|
|
38
44
|
diffAdd: "#1a3d1a",
|
|
39
45
|
diffRemove: "#3d1a1a",
|
|
40
46
|
diffAddFg: "#9CDCFE",
|
|
41
47
|
diffRemoveFg: "#F48771",
|
|
42
48
|
};
|
|
49
|
+
/**
|
|
50
|
+
* Light palette. Two ground rules drove the color choices:
|
|
51
|
+
* 1. Named ANSI colors that render OK on both backgrounds (red/green/blue)
|
|
52
|
+
* are kept by name so the user's terminal palette overrides remain
|
|
53
|
+
* effective.
|
|
54
|
+
* 2. Specific hex values are used wherever the dark palette assumed a dark
|
|
55
|
+
* background (notably accent/code/trace colors and message bubbles).
|
|
56
|
+
* Each hex was picked to clear WCAG AA contrast (4.5:1) against a near-
|
|
57
|
+
* white background (#fafafa) or, when applicable, against the explicit
|
|
58
|
+
* surface color in the same palette (e.g. diffAddFg vs diffAdd).
|
|
59
|
+
*/
|
|
60
|
+
export const lightTheme = {
|
|
61
|
+
user: "green",
|
|
62
|
+
agent: "blue",
|
|
63
|
+
error: "red",
|
|
64
|
+
warning: "#9A6500", // ANSI yellow is invisible on white — go to dark amber.
|
|
65
|
+
success: "green",
|
|
66
|
+
accent: "#0E5A85", // dark teal — replaces "cyan" which washes out on white.
|
|
67
|
+
border: "gray",
|
|
68
|
+
borderActive: "#0E5A85",
|
|
69
|
+
inputBorder: "#6B5FB8",
|
|
70
|
+
inputBorderDisabled: "#c5c3d0",
|
|
71
|
+
inputBg: "#f5f5fa",
|
|
72
|
+
inputBgDisabled: "#ebebf2",
|
|
73
|
+
inputText: "#1c1c24",
|
|
74
|
+
inputPlaceholder: "#7a7886",
|
|
75
|
+
muted: "gray",
|
|
76
|
+
dim: "gray",
|
|
77
|
+
thinking: "magenta",
|
|
78
|
+
thinkingDim: "gray",
|
|
79
|
+
toolName: "#0E5A85",
|
|
80
|
+
toolResult: "gray",
|
|
81
|
+
toolError: "red",
|
|
82
|
+
toolPending: "#9A6500",
|
|
83
|
+
code: "#9A6500",
|
|
84
|
+
traceAction: "#B85A20",
|
|
85
|
+
traceCount: "#5a5a5a",
|
|
86
|
+
traceDetail: "gray",
|
|
87
|
+
traceCommand: "#1A5FA0",
|
|
88
|
+
tracePending: "#9A6500",
|
|
89
|
+
userMessageBorder: "#6B5FB8",
|
|
90
|
+
userMessageBg: "#e8e6f4",
|
|
91
|
+
userMessageText: "#1c1c24",
|
|
92
|
+
userRail: "#6B5FB8",
|
|
93
|
+
diffAdd: "#d4f4d4",
|
|
94
|
+
diffRemove: "#f4d4d4",
|
|
95
|
+
diffAddFg: "#1c1c24",
|
|
96
|
+
diffRemoveFg: "#1c1c24",
|
|
97
|
+
};
|
|
98
|
+
const ThemeContext = createContext(darkTheme);
|
|
99
|
+
export const ThemeProvider = ThemeContext.Provider;
|
|
100
|
+
export function useTheme() {
|
|
101
|
+
return useContext(ThemeContext);
|
|
102
|
+
}
|
|
103
|
+
/** Build the active palette given a resolved mode and optional overrides. */
|
|
104
|
+
export function paletteFor(mode, overrides) {
|
|
105
|
+
const base = mode === "light" ? lightTheme : darkTheme;
|
|
106
|
+
if (!overrides)
|
|
107
|
+
return base;
|
|
108
|
+
const filtered = {};
|
|
109
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
110
|
+
if (typeof value === "string" && key in base) {
|
|
111
|
+
filtered[key] = value;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { ...base, ...filtered };
|
|
115
|
+
}
|