@bubblebrain-ai/bubble 0.0.10 → 0.0.12
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.d.ts +1 -0
- package/dist/agent.js +6 -2
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +31 -3
- package/dist/feedback/collect.d.ts +7 -0
- package/dist/feedback/collect.js +119 -0
- package/dist/feedback/config.d.ts +14 -0
- package/dist/feedback/config.js +16 -0
- package/dist/feedback/redact.d.ts +1 -0
- package/dist/feedback/redact.js +25 -0
- package/dist/feedback/submit.d.ts +6 -0
- package/dist/feedback/submit.js +43 -0
- package/dist/feedback/types.d.ts +22 -0
- package/dist/feishu/agent-host/approval-card.d.ts +11 -0
- package/dist/feishu/agent-host/approval-card.js +46 -0
- package/dist/feishu/agent-host/approval-ui.d.ts +59 -0
- package/dist/feishu/agent-host/approval-ui.js +214 -0
- package/dist/feishu/agent-host/run-driver.d.ts +51 -0
- package/dist/feishu/agent-host/run-driver.js +302 -0
- package/dist/feishu/agent-host/runtime-deps.d.ts +33 -0
- package/dist/feishu/agent-host/runtime-deps.js +8 -0
- package/dist/feishu/card/budget.d.ts +40 -0
- package/dist/feishu/card/budget.js +134 -0
- package/dist/feishu/card/renderer.d.ts +29 -0
- package/dist/feishu/card/renderer.js +245 -0
- package/dist/feishu/card/run-state-types.d.ts +49 -0
- package/dist/feishu/card/run-state-types.js +15 -0
- package/dist/feishu/card/run-state.d.ts +21 -0
- package/dist/feishu/card/run-state.js +217 -0
- package/dist/feishu/channel/channel.d.ts +52 -0
- package/dist/feishu/channel/channel.js +74 -0
- package/dist/feishu/config.d.ts +24 -0
- package/dist/feishu/config.js +97 -0
- package/dist/feishu/format.d.ts +6 -0
- package/dist/feishu/format.js +14 -0
- package/dist/feishu/index.d.ts +4 -0
- package/dist/feishu/index.js +4 -0
- package/dist/feishu/logger.d.ts +31 -0
- package/dist/feishu/logger.js +62 -0
- package/dist/feishu/paths.d.ts +12 -0
- package/dist/feishu/paths.js +38 -0
- package/dist/feishu/process-registry.d.ts +29 -0
- package/dist/feishu/process-registry.js +90 -0
- package/dist/feishu/router/commands.d.ts +38 -0
- package/dist/feishu/router/commands.js +286 -0
- package/dist/feishu/router/event-router.d.ts +40 -0
- package/dist/feishu/router/event-router.js +208 -0
- package/dist/feishu/router/whitelist.d.ts +23 -0
- package/dist/feishu/router/whitelist.js +20 -0
- package/dist/feishu/runtime/active-runs.d.ts +32 -0
- package/dist/feishu/runtime/active-runs.js +84 -0
- package/dist/feishu/runtime/pending-queue.d.ts +36 -0
- package/dist/feishu/runtime/pending-queue.js +98 -0
- package/dist/feishu/runtime/process-pool.d.ts +29 -0
- package/dist/feishu/runtime/process-pool.js +49 -0
- package/dist/feishu/schema.d.ts +17 -0
- package/dist/feishu/schema.js +252 -0
- package/dist/feishu/scope/scope-registry.d.ts +39 -0
- package/dist/feishu/scope/scope-registry.js +148 -0
- package/dist/feishu/scope/session-binder.d.ts +44 -0
- package/dist/feishu/scope/session-binder.js +100 -0
- package/dist/feishu/scope/session-store.d.ts +24 -0
- package/dist/feishu/scope/session-store.js +73 -0
- package/dist/feishu/secrets.d.ts +37 -0
- package/dist/feishu/secrets.js +129 -0
- package/dist/feishu/serve.d.ts +12 -0
- package/dist/feishu/serve.js +288 -0
- package/dist/feishu/types.d.ts +75 -0
- package/dist/feishu/types.js +23 -0
- package/dist/feishu/wizard.d.ts +24 -0
- package/dist/feishu/wizard.js +121 -0
- package/dist/main.js +98 -32
- package/dist/model-catalog.js +3 -0
- package/dist/prompt/compose.js +3 -3
- package/dist/prompt/environment.js +2 -0
- package/dist/prompt/reminders.js +1 -1
- package/dist/provider-openai-codex.d.ts +8 -1
- package/dist/provider-openai-codex.js +33 -9
- package/dist/provider.d.ts +2 -0
- package/dist/session-title.d.ts +16 -0
- package/dist/session-title.js +134 -0
- package/dist/session-types.d.ts +5 -0
- package/dist/session.d.ts +16 -0
- package/dist/session.js +154 -2
- package/dist/skills/invocation.js +0 -18
- package/dist/skills/registry.d.ts +1 -0
- package/dist/skills/registry.js +2 -0
- package/dist/slash-commands/commands.js +15 -22
- package/dist/slash-commands/feishu.d.ts +17 -0
- package/dist/slash-commands/feishu.js +400 -0
- package/dist/slash-commands/registry.js +1 -1
- package/dist/slash-commands/types.d.ts +3 -1
- package/dist/text-display.d.ts +3 -0
- package/dist/text-display.js +25 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +3 -1
- package/dist/tools/skill-search.d.ts +10 -0
- package/dist/tools/skill-search.js +134 -0
- package/dist/tools/skill.js +1 -4
- package/dist/tui-ink/app.js +265 -118
- package/dist/tui-ink/code-highlight.js +2 -3
- package/dist/tui-ink/detect-theme.d.ts +1 -18
- package/dist/tui-ink/detect-theme.js +1 -37
- package/dist/tui-ink/display-history.d.ts +20 -3
- package/dist/tui-ink/display-history.js +26 -27
- package/dist/tui-ink/feedback-dialog.d.ts +19 -0
- package/dist/tui-ink/feedback-dialog.js +123 -0
- package/dist/tui-ink/feishu-setup-picker.d.ts +5 -0
- package/dist/tui-ink/feishu-setup-picker.js +261 -0
- package/dist/tui-ink/input-box.d.ts +25 -1
- package/dist/tui-ink/input-box.js +132 -11
- package/dist/tui-ink/input-history.js +3 -5
- package/dist/tui-ink/markdown.d.ts +32 -0
- package/dist/tui-ink/markdown.js +111 -4
- package/dist/tui-ink/message-list.d.ts +1 -6
- package/dist/tui-ink/message-list.js +86 -34
- package/dist/tui-ink/model-picker.d.ts +18 -0
- package/dist/tui-ink/model-picker.js +81 -27
- package/dist/tui-ink/run-session-picker.d.ts +10 -0
- package/dist/tui-ink/run-session-picker.js +22 -0
- package/dist/tui-ink/run.js +7 -2
- package/dist/tui-ink/session-picker.d.ts +10 -0
- package/dist/tui-ink/session-picker.js +110 -0
- package/dist/tui-ink/terminal-mouse.d.ts +4 -0
- package/dist/tui-ink/terminal-mouse.js +23 -0
- package/dist/tui-ink/theme.js +2 -2
- package/dist/tui-ink/trace-groups.js +25 -2
- package/dist/tui-ink/welcome.js +2 -4
- package/package.json +4 -5
- package/dist/tui/clipboard.d.ts +0 -1
- package/dist/tui/clipboard.js +0 -53
- package/dist/tui/display-history.d.ts +0 -44
- package/dist/tui/display-history.js +0 -243
- package/dist/tui/escape-confirmation.d.ts +0 -15
- package/dist/tui/escape-confirmation.js +0 -30
- package/dist/tui/file-mentions.d.ts +0 -29
- package/dist/tui/file-mentions.js +0 -174
- package/dist/tui/global-key-router.d.ts +0 -3
- package/dist/tui/global-key-router.js +0 -87
- package/dist/tui/image-paste.d.ts +0 -95
- package/dist/tui/image-paste.js +0 -505
- package/dist/tui/markdown-inline.d.ts +0 -22
- package/dist/tui/markdown-inline.js +0 -68
- package/dist/tui/markdown-theme-rules.d.ts +0 -23
- package/dist/tui/markdown-theme-rules.js +0 -164
- package/dist/tui/markdown-theme.d.ts +0 -5
- package/dist/tui/markdown-theme.js +0 -27
- package/dist/tui/opencode-spinner.d.ts +0 -21
- package/dist/tui/opencode-spinner.js +0 -216
- package/dist/tui/prompt-keybindings.d.ts +0 -42
- package/dist/tui/prompt-keybindings.js +0 -35
- package/dist/tui/recent-activity.d.ts +0 -8
- package/dist/tui/recent-activity.js +0 -71
- package/dist/tui/render-signature.d.ts +0 -1
- package/dist/tui/render-signature.js +0 -7
- package/dist/tui/run.d.ts +0 -38
- package/dist/tui/run.js +0 -6996
- package/dist/tui/sidebar-mcp.d.ts +0 -31
- package/dist/tui/sidebar-mcp.js +0 -62
- package/dist/tui/sidebar-state.d.ts +0 -12
- package/dist/tui/sidebar-state.js +0 -69
- package/dist/tui/streaming-tool-args.d.ts +0 -15
- package/dist/tui/streaming-tool-args.js +0 -30
- package/dist/tui/tool-renderers/fallback.d.ts +0 -2
- package/dist/tui/tool-renderers/fallback.js +0 -75
- package/dist/tui/tool-renderers/registry.d.ts +0 -3
- package/dist/tui/tool-renderers/registry.js +0 -11
- package/dist/tui/tool-renderers/subagent.d.ts +0 -2
- package/dist/tui/tool-renderers/subagent.js +0 -114
- package/dist/tui/tool-renderers/types.d.ts +0 -36
- package/dist/tui/tool-renderers/write-preview.d.ts +0 -12
- package/dist/tui/tool-renderers/write-preview.js +0 -30
- package/dist/tui/tool-renderers/write.d.ts +0 -6
- package/dist/tui/tool-renderers/write.js +0 -88
- /package/dist/{tui/tool-renderers → feedback}/types.js +0 -0
|
@@ -3,8 +3,8 @@ import React from "react";
|
|
|
3
3
|
import { Box, Static, Text } from "ink";
|
|
4
4
|
import { useTheme } from "./theme.js";
|
|
5
5
|
import { highlightCode, inferLang } from "./code-highlight.js";
|
|
6
|
-
import { MarkdownContent } from "./markdown.js";
|
|
7
|
-
import { buildTraceGroups,
|
|
6
|
+
import { MarkdownContent, StreamingMarkdown } from "./markdown.js";
|
|
7
|
+
import { buildTraceGroups, formatTracePath, traceGroupLabel } from "./trace-groups.js";
|
|
8
8
|
import { EDIT_COLLAPSED_DIFF_LINES, formatEditSuccessSummary, getEditDiffDetails } from "./edit-diff.js";
|
|
9
9
|
import { formatSubagentRoute } from "../agent/subagent-route-format.js";
|
|
10
10
|
export function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, verboseTrace, pendingApproval, nowTick, welcomeBanner, }) {
|
|
@@ -12,12 +12,6 @@ export function MessageList({ messages, streamingContent, streamingReasoning, st
|
|
|
12
12
|
streamingReasoning ||
|
|
13
13
|
streamingTools.length > 0 ||
|
|
14
14
|
streamingParts.length > 0);
|
|
15
|
-
// Committed messages enter ink's <Static> region immediately and never move
|
|
16
|
-
// between a live <Box> and Static. Moving the same message across those
|
|
17
|
-
// regions writes it into terminal scrollback twice. Mutable assistant output
|
|
18
|
-
// stays in StreamingMessage until the agent reports a final turn_end. Keep
|
|
19
|
-
// the Static instance identity stable across terminal resizes; remounting it
|
|
20
|
-
// would replay all previously-written scrollback items.
|
|
21
15
|
const staticItems = [];
|
|
22
16
|
if (welcomeBanner) {
|
|
23
17
|
staticItems.push({ kind: "welcome", key: "welcome" });
|
|
@@ -32,7 +26,7 @@ export function MessageList({ messages, streamingContent, streamingReasoning, st
|
|
|
32
26
|
showExpandHint: !hasStreaming && i === lastMessageIndex,
|
|
33
27
|
});
|
|
34
28
|
}
|
|
35
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: staticItems, children: (item) => {
|
|
29
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(Static, { items: staticItems, children: (item) => {
|
|
36
30
|
if (item.kind === "welcome") {
|
|
37
31
|
return _jsx(React.Fragment, { children: welcomeBanner }, item.key);
|
|
38
32
|
}
|
|
@@ -47,13 +41,16 @@ function MessageItem({ message, terminalColumns, verboseTrace, showExpandHint, n
|
|
|
47
41
|
if (message.role === "error") {
|
|
48
42
|
return (_jsx(Box, { marginBottom: 1, flexDirection: "column", children: _jsxs(Text, { color: theme.error, children: ["Error: ", message.content] }) }));
|
|
49
43
|
}
|
|
44
|
+
if (message.syntheticKind === "ui_compact_summary") {
|
|
45
|
+
return _jsx(CompactionSummaryBlock, { message: message });
|
|
46
|
+
}
|
|
50
47
|
const hasVisibleAssistantContent = !!message.content ||
|
|
51
48
|
(message.toolCalls?.length ?? 0) > 0 ||
|
|
52
49
|
(message.parts?.length ?? 0) > 0 ||
|
|
53
50
|
(!!message.reasoning && verboseTrace);
|
|
54
51
|
if (!hasVisibleAssistantContent)
|
|
55
52
|
return null;
|
|
56
|
-
return (_jsxs(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: [message.reasoning && verboseTrace && _jsx(ReasoningTraceBlock, { reasoning: message.reasoning }), message.parts && message.parts.length > 0 ? (_jsx(MessageParts, { parts: message.parts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })) : (_jsxs(_Fragment, { children: [message.toolCalls && (_jsx(ToolsPart, { toolCalls: message.toolCalls, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })), message.content && _jsx(MarkdownContent, { content: message.content })] })), verboseTrace && message.toolCalls && message.toolCalls.length > 0 && (_jsx(TurnDigest, { toolCalls: message.toolCalls }))] }));
|
|
53
|
+
return (_jsxs(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: [message.reasoning && verboseTrace && _jsx(ReasoningTraceBlock, { reasoning: message.reasoning }), message.parts && message.parts.length > 0 ? (_jsx(MessageParts, { parts: message.parts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })) : (_jsxs(_Fragment, { children: [message.toolCalls && (_jsx(ToolsPart, { toolCalls: message.toolCalls, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })), message.content && _jsx(MarkdownContent, { content: message.content })] })), verboseTrace && message.toolCalls && message.toolCalls.length > 0 && (_jsx(TurnDigest, { toolCalls: message.toolCalls })), message.taskElapsedMs !== undefined && (_jsx(TaskDurationLine, { elapsedMs: message.taskElapsedMs }))] }));
|
|
57
54
|
}
|
|
58
55
|
function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, verboseTrace, pendingApproval, nowTick, }) {
|
|
59
56
|
const deferredContent = React.useDeferredValue(content);
|
|
@@ -62,18 +59,26 @@ function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, v
|
|
|
62
59
|
const visibleParts = deferredParts.length > 0
|
|
63
60
|
? deferredParts
|
|
64
61
|
: fallbackStreamingParts(deferredContent, tools);
|
|
65
|
-
return (_jsxs(Box, { flexDirection: "column", children: [deferredReasoning && verboseTrace && (_jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(ReasoningTraceBlock, { reasoning: deferredReasoning }) })), visibleParts.length > 0 && (_jsx(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: _jsx(MessageParts, { parts: visibleParts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: pendingApproval, showExpandHint: true, nowTick: nowTick, showActivity: true }) }))] }));
|
|
62
|
+
return (_jsxs(Box, { flexDirection: "column", children: [deferredReasoning && verboseTrace && (_jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(ReasoningTraceBlock, { reasoning: deferredReasoning }) })), visibleParts.length > 0 && (_jsx(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: _jsx(MessageParts, { parts: visibleParts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: pendingApproval, showExpandHint: true, nowTick: nowTick, showActivity: true, streaming: true }) }))] }));
|
|
66
63
|
}
|
|
67
|
-
function MessageParts({ parts, terminalColumns, verboseTrace, pendingApproval, showExpandHint, nowTick, showActivity = false, }) {
|
|
64
|
+
function MessageParts({ parts, terminalColumns, verboseTrace, pendingApproval, showExpandHint, nowTick, showActivity = false, streaming = false, }) {
|
|
68
65
|
const lastToolsPartIndex = findLastToolsPartIndex(parts);
|
|
66
|
+
const lastTextPartIndex = findLastTextPartIndex(parts);
|
|
69
67
|
return (_jsx(Box, { flexDirection: "column", children: parts.map((part, idx) => {
|
|
70
68
|
if (part.type === "text") {
|
|
71
|
-
return (_jsx(TimelineText, { content: part.content, compactTop: idx === 0, terminalColumns: terminalColumns }, `text-${idx}`));
|
|
69
|
+
return (_jsx(TimelineText, { content: part.content, compactTop: idx === 0, terminalColumns: terminalColumns, streaming: streaming && idx === lastTextPartIndex }, `text-${idx}`));
|
|
72
70
|
}
|
|
73
71
|
return (_jsx(ToolsPart, { toolCalls: part.toolCalls, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: pendingApproval, showExpandHint: showExpandHint && idx === lastToolsPartIndex, compactTop: idx === 0, nowTick: nowTick, showActivity: showActivity && idx === lastToolsPartIndex }, `tools-${idx}`));
|
|
74
72
|
}) }));
|
|
75
73
|
}
|
|
76
|
-
function
|
|
74
|
+
function findLastTextPartIndex(parts) {
|
|
75
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
76
|
+
if (parts[i]?.type === "text")
|
|
77
|
+
return i;
|
|
78
|
+
}
|
|
79
|
+
return -1;
|
|
80
|
+
}
|
|
81
|
+
function TimelineText({ content, compactTop, terminalColumns, streaming = false, }) {
|
|
77
82
|
const theme = useTheme();
|
|
78
83
|
if (!content.trim())
|
|
79
84
|
return null;
|
|
@@ -81,7 +86,8 @@ function TimelineText({ content, compactTop, terminalColumns, }) {
|
|
|
81
86
|
// timeline gutter; pass the remaining width so wide blocks like tables size
|
|
82
87
|
// themselves against the actual content area instead of the raw terminal.
|
|
83
88
|
const available = terminalColumns ? Math.max(20, terminalColumns - 5) : undefined;
|
|
84
|
-
|
|
89
|
+
const trimmed = content.trim();
|
|
90
|
+
return (_jsxs(Box, { marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsx(Text, { color: theme.agent, children: "\u26EC " }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: streaming ? (_jsx(StreamingMarkdown, { content: trimmed, maxWidth: available })) : (_jsx(MarkdownContent, { content: trimmed, maxWidth: available })) })] }));
|
|
85
91
|
}
|
|
86
92
|
function ToolsPart({ toolCalls, terminalColumns, verboseTrace, pendingApproval, showExpandHint, compactTop = false, nowTick, showActivity = false, }) {
|
|
87
93
|
if (toolCalls.length === 0)
|
|
@@ -120,10 +126,10 @@ function TraceGroupList({ toolCalls, terminalColumns, pendingApproval, nowTick,
|
|
|
120
126
|
function TraceActivityLine({ group, pendingApproval, nowTick, terminalColumns, }) {
|
|
121
127
|
const theme = useTheme();
|
|
122
128
|
const waiting = isTraceGroupWaitingForApproval(group, pendingApproval);
|
|
123
|
-
|
|
129
|
+
void nowTick;
|
|
124
130
|
const labelWidth = Math.max(20, terminalColumns - 26);
|
|
125
131
|
const label = truncateVisual(traceGroupLabel(group), labelWidth);
|
|
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 })
|
|
132
|
+
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 })] }));
|
|
127
133
|
}
|
|
128
134
|
function TraceGroupBlock({ group, terminalColumns, pendingApproval, compactTop, nowTick, }) {
|
|
129
135
|
const theme = useTheme();
|
|
@@ -137,23 +143,30 @@ function TraceGroupBlock({ group, terminalColumns, pendingApproval, compactTop,
|
|
|
137
143
|
const allErrored = group.hasError && group.errorCount >= group.raw.length && !group.pending;
|
|
138
144
|
const titleColor = allErrored ? theme.error : theme.traceAction;
|
|
139
145
|
const detailColor = allErrored ? theme.error : theme.traceDetail;
|
|
140
|
-
const commandWidth = Math.max(14, terminalColumns - group.title.length -
|
|
146
|
+
const commandWidth = Math.max(14, terminalColumns - group.title.length - 20);
|
|
141
147
|
const detailWidth = Math.max(20, terminalColumns - 8);
|
|
142
148
|
const detailLines = group.previewLines.length > 0 ? group.previewLines : group.items;
|
|
143
|
-
|
|
149
|
+
// When a bash command is too long to fit on the title line, drop it onto its
|
|
150
|
+
// own indented rows so narrow splits keep the full command visible instead of
|
|
151
|
+
// silently truncating mid-flag.
|
|
152
|
+
const commandFitsInline = !group.command || visualWidth(group.command) <= commandWidth;
|
|
153
|
+
const wrappedCommandLines = group.command && !commandFitsInline
|
|
154
|
+
? wrapByVisualWidth(group.command, Math.max(10, detailWidth - 2))
|
|
155
|
+
: null;
|
|
156
|
+
return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, color: titleColor, children: group.title }), group.command && commandFitsInline ? (_jsxs(Text, { color: theme.traceCommand, children: [" ", group.command] })) : !group.command && 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] })] }), wrappedCommandLines && (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: wrappedCommandLines.map((seg, idx) => (_jsx(Text, { color: theme.traceCommand, children: seg }, `cmd-${idx}`))) })), 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"] }) }))] }));
|
|
144
157
|
}
|
|
145
158
|
function EditTraceBlock({ tool, details, terminalColumns, compactTop, status, }) {
|
|
146
159
|
const theme = useTheme();
|
|
147
160
|
const path = formatTracePath(details.path ?? tool.args.path ?? "");
|
|
148
161
|
const pathWidth = Math.max(14, terminalColumns - 12);
|
|
149
|
-
return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsxs(
|
|
162
|
+
return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, color: theme.traceAction, children: "Edit" }), path && _jsxs(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 })] }));
|
|
150
163
|
}
|
|
151
164
|
function traceGroupStatus(group, waitingApproval, theme, nowTick) {
|
|
152
165
|
if (waitingApproval)
|
|
153
166
|
return { text: "waiting for approval", color: theme.warning };
|
|
154
167
|
if (group.pending) {
|
|
155
|
-
|
|
156
|
-
return { text:
|
|
168
|
+
void nowTick;
|
|
169
|
+
return { text: "running", color: theme.tracePending };
|
|
157
170
|
}
|
|
158
171
|
if (group.hasError) {
|
|
159
172
|
const count = group.errorCount || 1;
|
|
@@ -186,15 +199,24 @@ function ReasoningTraceBlock({ reasoning }) {
|
|
|
186
199
|
const lines = React.useMemo(() => reasoning.split("\n").filter((l) => l.trim() !== ""), [reasoning]);
|
|
187
200
|
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)))] }));
|
|
188
201
|
}
|
|
202
|
+
function CompactionSummaryBlock({ message }) {
|
|
203
|
+
const theme = useTheme();
|
|
204
|
+
const rawStatus = message.content.replace(/^✓\s*/, "").trim();
|
|
205
|
+
const status = rawStatus.replace(/^Compaction complete\s*(?:·\s*)?/i, "").trim() || "Session compacted";
|
|
206
|
+
const summary = message.compactionSummary?.trim();
|
|
207
|
+
return (_jsxs(Box, { marginTop: 1, marginBottom: 1, paddingX: 1, flexDirection: "column", borderStyle: "round", borderColor: theme.borderActive, children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.success, bold: true, children: "\u2713 " }), _jsx(Text, { color: theme.accent, bold: true, children: "Compaction checkpoint" }), _jsxs(Text, { color: theme.muted, children: [" \u00B7 ", status] })] }), summary && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.muted, dimColor: true, children: "Preserved context summary" }), _jsx(Box, { paddingLeft: 2, flexDirection: "column", children: _jsx(MarkdownContent, { content: summary }) })] }))] }));
|
|
208
|
+
}
|
|
189
209
|
function UserMessageBlock({ content, terminalColumns }) {
|
|
190
210
|
const theme = useTheme();
|
|
191
|
-
// Rail
|
|
211
|
+
// Rail and its right gutter must share the bubble background; otherwise the
|
|
212
|
+
// terminal background shows up as a dark seam between rail and message.
|
|
213
|
+
const railWidth = 2;
|
|
192
214
|
const horizontalRoom = Math.max(20, terminalColumns - 2);
|
|
193
|
-
const bubbleTextWidth = Math.max(1, horizontalRoom - 2);
|
|
215
|
+
const bubbleTextWidth = Math.max(1, horizontalRoom - railWidth - 2);
|
|
194
216
|
const wrappedLines = content
|
|
195
217
|
.split("\n")
|
|
196
218
|
.flatMap((line) => wrapByVisualWidth(line, bubbleTextWidth));
|
|
197
|
-
return (_jsx(Box, { flexDirection: "column", children: wrappedLines.map((line, index) => (_jsxs(Box, { children: [_jsx(Text, { color: theme.userRail, children: "
|
|
219
|
+
return (_jsx(Box, { flexDirection: "column", children: wrappedLines.map((line, index) => (_jsxs(Box, { children: [_jsx(Text, { backgroundColor: theme.userMessageBg, color: theme.userRail, children: index === 0 ? "▌ " : " " }), _jsx(Text, { backgroundColor: theme.userMessageBg, color: theme.userMessageText, children: ` ${padVisual(line || " ", bubbleTextWidth)} ` })] }, index))) }));
|
|
198
220
|
}
|
|
199
221
|
const TOOL_DISPLAY_NAMES = {
|
|
200
222
|
read: "Read",
|
|
@@ -403,8 +425,8 @@ function ToolCallDisplay({ toolCall, isStreaming, verbose, terminalColumns, show
|
|
|
403
425
|
? theme.toolPending
|
|
404
426
|
: theme.user;
|
|
405
427
|
const name = displayToolName(toolCall.name);
|
|
406
|
-
// Compose summary: pending tools
|
|
407
|
-
//
|
|
428
|
+
// Compose summary: pending tools stay compact; waiting-for-approval gets an
|
|
429
|
+
// explicit badge so the trail survives the dialog closing.
|
|
408
430
|
let summary;
|
|
409
431
|
let summaryColor = theme.muted;
|
|
410
432
|
if (waitingApproval) {
|
|
@@ -412,8 +434,8 @@ function ToolCallDisplay({ toolCall, isStreaming, verbose, terminalColumns, show
|
|
|
412
434
|
summaryColor = theme.warning;
|
|
413
435
|
}
|
|
414
436
|
else if (toolCall.result === undefined && toolCall.startedAt) {
|
|
415
|
-
|
|
416
|
-
summary =
|
|
437
|
+
void nowTick;
|
|
438
|
+
summary = "running";
|
|
417
439
|
summaryColor = theme.toolPending;
|
|
418
440
|
}
|
|
419
441
|
else {
|
|
@@ -529,7 +551,8 @@ function DiffBlock({ diff, terminalColumns, maxLines, verbose, showExpandHint, }
|
|
|
529
551
|
// row beneath it.
|
|
530
552
|
const bandWidth = Math.max(10, terminalColumns - 7);
|
|
531
553
|
const contentWidth = Math.max(1, bandWidth - prefixWidth);
|
|
532
|
-
|
|
554
|
+
const blankPrefix = " ".repeat(prefixWidth);
|
|
555
|
+
return (_jsxs(Box, { flexDirection: "column", marginLeft: leftMargin, children: [shown.flatMap((line, i) => {
|
|
533
556
|
const bg = line.type === "add"
|
|
534
557
|
? theme.diffAdd
|
|
535
558
|
: line.type === "remove"
|
|
@@ -537,10 +560,17 @@ function DiffBlock({ diff, terminalColumns, maxLines, verbose, showExpandHint, }
|
|
|
537
560
|
: undefined;
|
|
538
561
|
const sign = line.type === "add" ? "+" : line.type === "remove" ? "-" : " ";
|
|
539
562
|
const numStr = String(line.num).padStart(numWidth, " ");
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
563
|
+
// Soft-wrap long lines at the terminal-derived content width so narrow
|
|
564
|
+
// splits still show the full content. Continuation rows reuse the same
|
|
565
|
+
// background but blank out the gutter (no line number, no +/-) so a
|
|
566
|
+
// reader can tell at a glance which rows belong to the same logical
|
|
567
|
+
// diff line.
|
|
568
|
+
const segments = wrapByVisualWidth(line.content, contentWidth);
|
|
569
|
+
return segments.map((segment, segIdx) => {
|
|
570
|
+
const padded = padVisual(segment, contentWidth);
|
|
571
|
+
const prefix = segIdx === 0 ? ` ${numStr} ${sign} ` : blankPrefix;
|
|
572
|
+
return (_jsx(Text, { backgroundColor: bg, color: theme.userMessageText, children: `${prefix}${padded}` }, `${i}-${segIdx}`));
|
|
573
|
+
});
|
|
544
574
|
}), _jsx(TruncationHint, { remaining: remaining, verbose: verbose, showExpandHint: showExpandHint })] }));
|
|
545
575
|
}
|
|
546
576
|
/**
|
|
@@ -554,6 +584,10 @@ function TurnDigest({ toolCalls }) {
|
|
|
554
584
|
return null;
|
|
555
585
|
return (_jsx(Box, { marginLeft: 2, marginTop: 1, children: _jsx(Text, { color: theme.muted, dimColor: true, children: digest }) }));
|
|
556
586
|
}
|
|
587
|
+
function TaskDurationLine({ elapsedMs }) {
|
|
588
|
+
const theme = useTheme();
|
|
589
|
+
return (_jsx(Box, { marginLeft: 2, marginTop: 1, children: _jsxs(Text, { color: theme.muted, dimColor: true, children: ["Task duration: ", formatDuration(elapsedMs)] }) }));
|
|
590
|
+
}
|
|
557
591
|
function buildDigest(toolCalls) {
|
|
558
592
|
const paths = new Set();
|
|
559
593
|
let added = 0;
|
|
@@ -595,6 +629,24 @@ function buildDigest(toolCalls) {
|
|
|
595
629
|
: "";
|
|
596
630
|
return `↳ ${verb} ${paths.size} file${paths.size === 1 ? "" : "s"}${stats} — ${pathDisplay}`;
|
|
597
631
|
}
|
|
632
|
+
function formatDuration(ms) {
|
|
633
|
+
if (!Number.isFinite(ms) || ms <= 0)
|
|
634
|
+
return "0s";
|
|
635
|
+
if (ms < 1000)
|
|
636
|
+
return `${Math.max(1, Math.round(ms))}ms`;
|
|
637
|
+
const seconds = ms / 1000;
|
|
638
|
+
if (seconds < 10)
|
|
639
|
+
return `${seconds.toFixed(1)}s`;
|
|
640
|
+
if (seconds < 60)
|
|
641
|
+
return `${Math.round(seconds)}s`;
|
|
642
|
+
let minutes = Math.floor(seconds / 60);
|
|
643
|
+
let remSec = Math.round(seconds - minutes * 60);
|
|
644
|
+
if (remSec >= 60) {
|
|
645
|
+
minutes += Math.floor(remSec / 60);
|
|
646
|
+
remSec %= 60;
|
|
647
|
+
}
|
|
648
|
+
return remSec === 0 ? `${minutes}m` : `${minutes}m ${remSec}s`;
|
|
649
|
+
}
|
|
598
650
|
function truncateVisual(str, maxWidth) {
|
|
599
651
|
if (maxWidth <= 0)
|
|
600
652
|
return "";
|
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
import { ProviderRegistry } from "../provider-registry.js";
|
|
2
|
+
export { padVisual, truncateVisual } from "../text-display.js";
|
|
2
3
|
export interface ModelPickerOption {
|
|
3
4
|
id: string;
|
|
4
5
|
label: string;
|
|
5
6
|
group: string;
|
|
6
7
|
providerBadge: string;
|
|
7
8
|
}
|
|
9
|
+
export type PickerKeyAction = "up" | "down" | "enter" | "escape" | "backspace" | "delete";
|
|
10
|
+
export declare function resolvePickerKeyAction(input: string, key: {
|
|
11
|
+
upArrow?: boolean;
|
|
12
|
+
downArrow?: boolean;
|
|
13
|
+
return?: boolean;
|
|
14
|
+
escape?: boolean;
|
|
15
|
+
backspace?: boolean;
|
|
16
|
+
delete?: boolean;
|
|
17
|
+
}): PickerKeyAction | undefined;
|
|
18
|
+
export declare function isPrintablePickerInput(input: string): boolean;
|
|
19
|
+
export declare function formatSkillPickerRow(skill: {
|
|
20
|
+
name: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
}, options: {
|
|
23
|
+
selected: boolean;
|
|
24
|
+
width: number;
|
|
25
|
+
}): string;
|
|
8
26
|
export interface ModelPickerProps {
|
|
9
27
|
registry: ProviderRegistry;
|
|
10
28
|
current: string;
|
|
@@ -4,13 +4,61 @@ import { Box, Text, useInput, usePaste, useStdout } from "ink";
|
|
|
4
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
|
+
import { padVisual, truncateVisual } from "../text-display.js";
|
|
8
|
+
export { padVisual, truncateVisual } from "../text-display.js";
|
|
9
|
+
export function resolvePickerKeyAction(input, key) {
|
|
10
|
+
if (key.escape)
|
|
11
|
+
return "escape";
|
|
12
|
+
if (key.return)
|
|
13
|
+
return "enter";
|
|
14
|
+
if (key.upArrow)
|
|
15
|
+
return "up";
|
|
16
|
+
if (key.downArrow)
|
|
17
|
+
return "down";
|
|
18
|
+
if (key.backspace)
|
|
19
|
+
return "backspace";
|
|
20
|
+
if (key.delete)
|
|
21
|
+
return "delete";
|
|
22
|
+
const sequence = normalizeEscapeSequence(input);
|
|
23
|
+
if (/^(?:O|\[[\d;:]*)A$/.test(sequence))
|
|
24
|
+
return "up";
|
|
25
|
+
if (/^(?:O|\[[\d;:]*)B$/.test(sequence))
|
|
26
|
+
return "down";
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
export function isPrintablePickerInput(input) {
|
|
30
|
+
if (!input)
|
|
31
|
+
return false;
|
|
32
|
+
if (input.startsWith("\x1b"))
|
|
33
|
+
return false;
|
|
34
|
+
if (isRawEscapeTail(input))
|
|
35
|
+
return false;
|
|
36
|
+
return !/[\x00-\x1f\x7f]/.test(input);
|
|
37
|
+
}
|
|
38
|
+
export function formatSkillPickerRow(skill, options) {
|
|
39
|
+
const width = Math.max(12, options.width);
|
|
40
|
+
const marker = options.selected ? "> " : " ";
|
|
41
|
+
const nameBudget = Math.max(8, Math.min(28, Math.floor(width * 0.35)));
|
|
42
|
+
const name = truncateVisual(skill.name, nameBudget);
|
|
43
|
+
const nameCell = padVisual(name, nameBudget);
|
|
44
|
+
const description = (skill.description ?? "").replace(/\s+/g, " ").trim();
|
|
45
|
+
const row = description
|
|
46
|
+
? `${marker}${nameCell} ${description}`
|
|
47
|
+
: `${marker}${nameCell}`;
|
|
48
|
+
return padVisual(truncateVisual(row, width), width);
|
|
49
|
+
}
|
|
50
|
+
function normalizeEscapeSequence(input) {
|
|
51
|
+
return input.startsWith("\x1b") ? input.slice(1) : input;
|
|
52
|
+
}
|
|
53
|
+
function isRawEscapeTail(input) {
|
|
54
|
+
return /^(?:O[ABCDHF]|\[[\d;:]*[A-Za-z~])$/.test(input);
|
|
55
|
+
}
|
|
7
56
|
export function ModelPicker({ registry, current, recent, onSelect, onCancel }) {
|
|
8
57
|
const theme = useTheme();
|
|
9
58
|
const { stdout } = useStdout();
|
|
10
59
|
const termHeight = stdout?.rows || 24;
|
|
11
60
|
const maxVisible = Math.max(5, termHeight - 10);
|
|
12
61
|
const [rawOptions, setRawOptions] = useState(() => buildLocalModelOptions(registry, current, recent));
|
|
13
|
-
const [refreshing, setRefreshing] = useState(false);
|
|
14
62
|
const [query, setQuery] = useState("");
|
|
15
63
|
const [selectedIndex, setSelectedIndex] = useState(() => preferredModelIndex(buildLocalModelOptions(registry, current, recent), current));
|
|
16
64
|
useEffect(() => {
|
|
@@ -63,10 +111,8 @@ export function ModelPicker({ registry, current, recent, onSelect, onCancel }) {
|
|
|
63
111
|
const currentIndex = preferredModelIndex(opts, current);
|
|
64
112
|
return index === preferredModelIndex(localOptions, current) ? currentIndex : Math.min(index, Math.max(0, opts.length - 1));
|
|
65
113
|
});
|
|
66
|
-
setRefreshing(false);
|
|
67
114
|
}
|
|
68
115
|
}
|
|
69
|
-
setRefreshing(true);
|
|
70
116
|
void refreshRemote();
|
|
71
117
|
return () => {
|
|
72
118
|
cancelled = true;
|
|
@@ -79,25 +125,26 @@ export function ModelPicker({ registry, current, recent, onSelect, onCancel }) {
|
|
|
79
125
|
return rawOptions.filter((opt) => opt.label.toLowerCase().includes(q) || opt.providerBadge.toLowerCase().includes(q));
|
|
80
126
|
}, [rawOptions, query]);
|
|
81
127
|
useInput((input, key) => {
|
|
82
|
-
|
|
128
|
+
const action = resolvePickerKeyAction(input, key);
|
|
129
|
+
if (action === "escape") {
|
|
83
130
|
onCancel();
|
|
84
131
|
return;
|
|
85
132
|
}
|
|
86
|
-
if (
|
|
133
|
+
if (action === "enter") {
|
|
87
134
|
const opt = options[selectedIndex];
|
|
88
135
|
if (opt)
|
|
89
136
|
onSelect(opt.id);
|
|
90
137
|
return;
|
|
91
138
|
}
|
|
92
|
-
if (
|
|
139
|
+
if (action === "up") {
|
|
93
140
|
setSelectedIndex((i) => Math.max(0, i - 1));
|
|
94
141
|
return;
|
|
95
142
|
}
|
|
96
|
-
if (
|
|
143
|
+
if (action === "down") {
|
|
97
144
|
setSelectedIndex((i) => Math.min(options.length - 1, i + 1));
|
|
98
145
|
return;
|
|
99
146
|
}
|
|
100
|
-
if (
|
|
147
|
+
if (action === "backspace" || action === "delete") {
|
|
101
148
|
setQuery((q) => {
|
|
102
149
|
const next = q.slice(0, -1);
|
|
103
150
|
setSelectedIndex(0);
|
|
@@ -105,7 +152,7 @@ export function ModelPicker({ registry, current, recent, onSelect, onCancel }) {
|
|
|
105
152
|
});
|
|
106
153
|
return;
|
|
107
154
|
}
|
|
108
|
-
if (input && !key.ctrl && !key.meta) {
|
|
155
|
+
if (isPrintablePickerInput(input) && !key.ctrl && !key.meta) {
|
|
109
156
|
setQuery((q) => {
|
|
110
157
|
const next = q + input;
|
|
111
158
|
setSelectedIndex(0);
|
|
@@ -116,7 +163,7 @@ export function ModelPicker({ registry, current, recent, onSelect, onCancel }) {
|
|
|
116
163
|
});
|
|
117
164
|
const start = Math.max(0, Math.min(selectedIndex, options.length - maxVisible));
|
|
118
165
|
const visible = options.slice(start, start + maxVisible);
|
|
119
|
-
return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: "Select Model" }), _jsx(SearchField, { query: query, placeholder: "Type to search models..." }), _jsx(Text, { color: theme.muted, children: "\u2191/\u2193 navigate \u00B7 Enter select \u00B7 Esc cancel \u00B7 Backspace clear" }),
|
|
166
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: "Select Model" }), _jsx(SearchField, { query: query, placeholder: "Type to search models..." }), _jsx(Text, { color: theme.muted, children: "\u2191/\u2193 navigate \u00B7 Enter select \u00B7 Esc cancel \u00B7 Backspace clear" }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [options.length === 0 && (_jsxs(Text, { color: theme.muted, children: ["No models match \"", query, "\""] })), visible.map((opt, i) => {
|
|
120
167
|
const actualIndex = start + i;
|
|
121
168
|
const isSelected = actualIndex === selectedIndex;
|
|
122
169
|
return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? theme.accent : undefined, children: [isSelected ? "> " : " ", opt.label] }), _jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: theme.muted, dimColor: true, children: opt.providerBadge }) }), opt.id === current && (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: theme.accent, children: "\u25CF" }) }))] }, opt.id));
|
|
@@ -200,25 +247,26 @@ export function ProviderPicker({ providers, current, onSelect, onCancel, title }
|
|
|
200
247
|
return idx >= 0 ? idx : 0;
|
|
201
248
|
});
|
|
202
249
|
useInput((input, key) => {
|
|
203
|
-
|
|
250
|
+
const action = resolvePickerKeyAction(input, key);
|
|
251
|
+
if (action === "escape") {
|
|
204
252
|
onCancel();
|
|
205
253
|
return;
|
|
206
254
|
}
|
|
207
|
-
if (
|
|
255
|
+
if (action === "enter") {
|
|
208
256
|
const p = providers[selectedIndex];
|
|
209
257
|
if (p)
|
|
210
258
|
onSelect(p.id);
|
|
211
259
|
return;
|
|
212
260
|
}
|
|
213
|
-
if (
|
|
261
|
+
if (action === "up") {
|
|
214
262
|
setSelectedIndex((i) => Math.max(0, i - 1));
|
|
215
263
|
return;
|
|
216
264
|
}
|
|
217
|
-
if (
|
|
265
|
+
if (action === "down") {
|
|
218
266
|
setSelectedIndex((i) => Math.min(providers.length - 1, i + 1));
|
|
219
267
|
return;
|
|
220
268
|
}
|
|
221
|
-
if (input && input.length === 1 && /[a-z]/i.test(input)) {
|
|
269
|
+
if (isPrintablePickerInput(input) && input.length === 1 && /[a-z]/i.test(input)) {
|
|
222
270
|
const char = input.toLowerCase();
|
|
223
271
|
for (let i = selectedIndex + 1; i < providers.length; i++) {
|
|
224
272
|
if (providers[i].name.toLowerCase().startsWith(char)) {
|
|
@@ -246,20 +294,21 @@ export function KeyPicker({ providerName, onSubmit, onCancel }) {
|
|
|
246
294
|
const theme = useTheme();
|
|
247
295
|
const [value, setValue] = useState("");
|
|
248
296
|
useInput((input, key) => {
|
|
249
|
-
|
|
297
|
+
const action = resolvePickerKeyAction(input, key);
|
|
298
|
+
if (action === "escape") {
|
|
250
299
|
onCancel();
|
|
251
300
|
return;
|
|
252
301
|
}
|
|
253
|
-
if (
|
|
302
|
+
if (action === "enter") {
|
|
254
303
|
if (value.trim())
|
|
255
304
|
onSubmit(value.trim());
|
|
256
305
|
return;
|
|
257
306
|
}
|
|
258
|
-
if (
|
|
307
|
+
if (action === "backspace" || action === "delete") {
|
|
259
308
|
setValue((v) => v.slice(0, -1));
|
|
260
309
|
return;
|
|
261
310
|
}
|
|
262
|
-
if (input && !key.ctrl && !key.meta) {
|
|
311
|
+
if (isPrintablePickerInput(input) && !key.ctrl && !key.meta) {
|
|
263
312
|
setValue((v) => v + input);
|
|
264
313
|
}
|
|
265
314
|
});
|
|
@@ -277,7 +326,9 @@ export function SkillPicker({ skills, onSelect, onCancel }) {
|
|
|
277
326
|
const theme = useTheme();
|
|
278
327
|
const { stdout } = useStdout();
|
|
279
328
|
const termHeight = stdout?.rows || 24;
|
|
329
|
+
const terminalColumns = stdout?.columns || 80;
|
|
280
330
|
const maxVisible = Math.max(5, termHeight - 8);
|
|
331
|
+
const rowWidth = Math.max(36, Math.min(96, terminalColumns - 6));
|
|
281
332
|
const [query, setQuery] = useState("");
|
|
282
333
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
283
334
|
const options = useMemo(() => {
|
|
@@ -287,25 +338,26 @@ export function SkillPicker({ skills, onSelect, onCancel }) {
|
|
|
287
338
|
return skills.filter((skill) => skill.name.toLowerCase().includes(q) || skill.description.toLowerCase().includes(q));
|
|
288
339
|
}, [query, skills]);
|
|
289
340
|
useInput((input, key) => {
|
|
290
|
-
|
|
341
|
+
const action = resolvePickerKeyAction(input, key);
|
|
342
|
+
if (action === "escape") {
|
|
291
343
|
onCancel();
|
|
292
344
|
return;
|
|
293
345
|
}
|
|
294
|
-
if (
|
|
346
|
+
if (action === "enter") {
|
|
295
347
|
const skill = options[selectedIndex];
|
|
296
348
|
if (skill)
|
|
297
349
|
onSelect(skill.name);
|
|
298
350
|
return;
|
|
299
351
|
}
|
|
300
|
-
if (
|
|
352
|
+
if (action === "up") {
|
|
301
353
|
setSelectedIndex((i) => Math.max(0, i - 1));
|
|
302
354
|
return;
|
|
303
355
|
}
|
|
304
|
-
if (
|
|
356
|
+
if (action === "down") {
|
|
305
357
|
setSelectedIndex((i) => Math.min(Math.max(0, options.length - 1), i + 1));
|
|
306
358
|
return;
|
|
307
359
|
}
|
|
308
|
-
if (
|
|
360
|
+
if (action === "backspace" || action === "delete") {
|
|
309
361
|
setQuery((q) => {
|
|
310
362
|
const next = q.slice(0, -1);
|
|
311
363
|
setSelectedIndex(0);
|
|
@@ -313,7 +365,7 @@ export function SkillPicker({ skills, onSelect, onCancel }) {
|
|
|
313
365
|
});
|
|
314
366
|
return;
|
|
315
367
|
}
|
|
316
|
-
if (input && !key.ctrl && !key.meta) {
|
|
368
|
+
if (isPrintablePickerInput(input) && !key.ctrl && !key.meta) {
|
|
317
369
|
setQuery((q) => {
|
|
318
370
|
const next = q + input;
|
|
319
371
|
setSelectedIndex(0);
|
|
@@ -321,11 +373,13 @@ export function SkillPicker({ skills, onSelect, onCancel }) {
|
|
|
321
373
|
});
|
|
322
374
|
}
|
|
323
375
|
});
|
|
324
|
-
const
|
|
376
|
+
const maxStart = Math.max(0, options.length - maxVisible);
|
|
377
|
+
const start = Math.max(0, Math.min(maxStart, selectedIndex - Math.floor(maxVisible / 2)));
|
|
325
378
|
const visible = options.slice(start, start + maxVisible);
|
|
326
379
|
return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: "Select Skill" }), _jsx(SearchField, { query: query, placeholder: "Type to search skills..." }), _jsx(Text, { color: theme.muted, children: "\u2191/\u2193 navigate \u00B7 Enter load \u00B7 Esc cancel \u00B7 Backspace clear" }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [options.length === 0 && (_jsxs(Text, { color: theme.muted, children: ["No skills match \"", query, "\""] })), visible.map((skill, i) => {
|
|
327
380
|
const actualIndex = start + i;
|
|
328
381
|
const isSelected = actualIndex === selectedIndex;
|
|
329
|
-
|
|
382
|
+
const row = formatSkillPickerRow(skill, { selected: isSelected, width: rowWidth });
|
|
383
|
+
return (_jsx(Box, { children: _jsx(Text, { inverse: isSelected, color: isSelected ? theme.accent : undefined, bold: isSelected, children: row }) }, skill.name));
|
|
330
384
|
})] })] }));
|
|
331
385
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ResolvedTheme } from "./detect-theme.js";
|
|
2
|
+
import type { SessionSummary } from "../session.js";
|
|
3
|
+
export interface RunSessionPickerOptions {
|
|
4
|
+
currentCwd: string;
|
|
5
|
+
currentSessions: SessionSummary[];
|
|
6
|
+
allSessions: SessionSummary[];
|
|
7
|
+
resolvedTheme: ResolvedTheme;
|
|
8
|
+
themeOverrides?: Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
export declare function runSessionPicker(options: RunSessionPickerOptions): Promise<string | undefined>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render } from "ink";
|
|
3
|
+
import { SessionPicker } from "./session-picker.js";
|
|
4
|
+
import { ThemeProvider, paletteFor } from "./theme.js";
|
|
5
|
+
export async function runSessionPicker(options) {
|
|
6
|
+
const theme = paletteFor(options.resolvedTheme, options.themeOverrides);
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
let done = false;
|
|
9
|
+
const finish = (value) => {
|
|
10
|
+
if (done)
|
|
11
|
+
return;
|
|
12
|
+
done = true;
|
|
13
|
+
try {
|
|
14
|
+
instance.unmount();
|
|
15
|
+
}
|
|
16
|
+
catch { /* ignore */ }
|
|
17
|
+
resolve(value);
|
|
18
|
+
};
|
|
19
|
+
const instance = render(_jsx(ThemeProvider, { value: theme, children: _jsx(SessionPicker, { currentCwd: options.currentCwd, currentSessions: options.currentSessions, allSessions: options.allSessions, onSelect: (file) => finish(file), onCancel: () => finish(undefined) }) }));
|
|
20
|
+
void instance.waitUntilExit().then(() => finish(undefined));
|
|
21
|
+
});
|
|
22
|
+
}
|
package/dist/tui-ink/run.js
CHANGED
|
@@ -4,7 +4,7 @@ import chalk from "chalk";
|
|
|
4
4
|
import { App } from "./app.js";
|
|
5
5
|
import { warmHighlighter } from "./code-highlight.js";
|
|
6
6
|
export async function runTui(agent, args, options = {}) {
|
|
7
|
-
// Kick off shiki load before the first code block
|
|
7
|
+
// Kick off shiki load before the first code block is rendered. Fire and
|
|
8
8
|
// forget — CodeBlock's lazy init falls back to raw lines if this isn't ready
|
|
9
9
|
// yet, so callers don't need to await it.
|
|
10
10
|
warmHighlighter();
|
|
@@ -18,6 +18,11 @@ export async function runTui(agent, args, options = {}) {
|
|
|
18
18
|
// teardown so it lands in the real shell scrollback (Claude-Code style).
|
|
19
19
|
exitSummary = summary;
|
|
20
20
|
} }), {
|
|
21
|
+
// Bubble owns Ctrl+C so it can route both raw ETX and kitty keyboard
|
|
22
|
+
// Ctrl+C through App.requestExit(). Ink's default only exits reliably
|
|
23
|
+
// for raw "\x03"; with kitty keyboard it can swallow the parsed
|
|
24
|
+
// ctrl+c event before our useInput handlers see it.
|
|
25
|
+
exitOnCtrlC: false,
|
|
21
26
|
kittyKeyboard: {
|
|
22
27
|
mode: "enabled",
|
|
23
28
|
flags: ["disambiguateEscapeCodes"],
|
|
@@ -36,7 +41,7 @@ export async function runTui(agent, args, options = {}) {
|
|
|
36
41
|
}
|
|
37
42
|
}
|
|
38
43
|
function formatExitSummary(summary) {
|
|
39
|
-
const label = "Total duration
|
|
44
|
+
const label = "Total duration:";
|
|
40
45
|
return chalk.dim(`${label} ${formatWallMs(summary.wallMs)}`);
|
|
41
46
|
}
|
|
42
47
|
function formatWallMs(ms) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SessionSummary } from "../session.js";
|
|
2
|
+
export type SessionPickerMode = "current" | "all";
|
|
3
|
+
export interface SessionPickerProps {
|
|
4
|
+
currentCwd: string;
|
|
5
|
+
currentSessions: SessionSummary[];
|
|
6
|
+
allSessions: SessionSummary[];
|
|
7
|
+
onSelect: (file: string) => void;
|
|
8
|
+
onCancel: () => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function SessionPicker({ currentCwd, currentSessions, allSessions, onSelect, onCancel }: SessionPickerProps): import("react/jsx-runtime").JSX.Element;
|