@bubblebrain-ai/bubble 0.0.49 → 0.0.51
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/abort-errors.d.ts +4 -0
- package/dist/agent/abort-errors.js +30 -0
- package/dist/agent/profiles.js +3 -4
- package/dist/agent/provider-fingerprint.d.ts +6 -0
- package/dist/agent/provider-fingerprint.js +89 -0
- package/dist/agent/resident-history.d.ts +7 -0
- package/dist/agent/resident-history.js +61 -0
- package/dist/agent/subtask-policy.js +4 -4
- package/dist/agent/transcript-helpers.d.ts +20 -0
- package/dist/agent/transcript-helpers.js +62 -0
- package/dist/agent/update-queue.d.ts +13 -0
- package/dist/agent/update-queue.js +69 -0
- package/dist/agent.d.ts +33 -26
- package/dist/agent.js +80 -334
- package/dist/debug-trace.js +0 -2
- package/dist/external-runtime/grok-runtime.js +1 -1
- package/dist/feishu/agent-host/run-driver.js +1 -8
- package/dist/feishu/card/run-state.js +0 -3
- package/dist/lsp/service.js +2 -2
- package/dist/main.js +0 -15
- package/dist/memory/phase1.js +0 -2
- package/dist/model-catalog.js +8 -6
- package/dist/permissions/settings.d.ts +0 -1
- package/dist/permissions/settings.js +0 -2
- package/dist/prompt/compose.js +0 -3
- package/dist/prompt/environment.js +0 -2
- package/dist/prompt/reminders.js +1 -1
- package/dist/provider-registry.js +13 -1
- package/dist/provider-transform.js +7 -8
- package/dist/sdk/index.d.ts +17 -2
- package/dist/sdk/index.js +37 -6
- package/dist/session-log.d.ts +2 -4
- package/dist/session-log.js +0 -23
- package/dist/session-types.d.ts +2 -6
- package/dist/session.d.ts +1 -3
- package/dist/session.js +0 -8
- package/dist/slash-commands/commands.js +13 -13
- package/dist/tools/child-tools.d.ts +1 -1
- package/dist/tools/child-tools.js +1 -1
- package/dist/tools/grep.js +1 -1
- package/dist/tools/index.d.ts +0 -3
- package/dist/tools/index.js +0 -3
- package/dist/tui-ink/app-helpers.d.ts +43 -0
- package/dist/tui-ink/app-helpers.js +150 -0
- package/dist/tui-ink/app.d.ts +4 -11
- package/dist/tui-ink/app.js +57 -657
- package/dist/tui-ink/command-palette.d.ts +32 -0
- package/dist/tui-ink/command-palette.js +158 -0
- package/dist/tui-ink/display-reconstruct.d.ts +3 -0
- package/dist/tui-ink/display-reconstruct.js +82 -0
- package/dist/tui-ink/message-list.js +0 -1
- package/dist/tui-ink/rewind-picker.d.ts +8 -0
- package/dist/tui-ink/rewind-picker.js +102 -0
- package/dist/tui-ink/session-picker.js +1 -1
- package/dist/tui-ink/stats-panel.d.ts +11 -0
- package/dist/tui-ink/stats-panel.js +65 -0
- package/dist/tui-ink/waiting-indicator.d.ts +12 -0
- package/dist/tui-ink/waiting-indicator.js +129 -0
- package/dist/types.d.ts +0 -9
- package/package.json +1 -1
- package/dist/tools/todo.d.ts +0 -12
- package/dist/tools/todo.js +0 -136
- package/dist/tui-ink/todos.d.ts +0 -7
- package/dist/tui-ink/todos.js +0 -46
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { SkillRegistry } from "../skills/registry.js";
|
|
2
|
+
import type { McpManager } from "../mcp/manager.js";
|
|
3
|
+
export interface PaletteItem {
|
|
4
|
+
label: string;
|
|
5
|
+
detail: string;
|
|
6
|
+
value: string;
|
|
7
|
+
command: string;
|
|
8
|
+
action?: "insert-skill";
|
|
9
|
+
}
|
|
10
|
+
export declare function buildCommandPaletteItems(skillRegistry: SkillRegistry, grokSessionBound?: boolean): PaletteItem[];
|
|
11
|
+
export declare function buildMcpReconnectItems(mcpManager?: McpManager): PaletteItem[];
|
|
12
|
+
export declare function CommandPalette({ items, terminalColumns, terminalRows, onSelect, onCancel, }: {
|
|
13
|
+
items: PaletteItem[];
|
|
14
|
+
terminalColumns: number;
|
|
15
|
+
terminalRows: number;
|
|
16
|
+
onSelect: (item: PaletteItem) => void;
|
|
17
|
+
onCancel: () => void;
|
|
18
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
export declare function McpReconnectPicker({ items, terminalColumns, terminalRows, onSelect, onCancel, }: {
|
|
20
|
+
items: PaletteItem[];
|
|
21
|
+
terminalColumns: number;
|
|
22
|
+
terminalRows: number;
|
|
23
|
+
onSelect: (item: PaletteItem) => void;
|
|
24
|
+
onCancel: () => void;
|
|
25
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
26
|
+
export declare const INK_LOCAL_SLASH_COMMANDS: readonly [{
|
|
27
|
+
readonly name: "goal";
|
|
28
|
+
readonly description: "Set/manage an autonomous goal (/goal <objective>|clear|pause|resume|edit)";
|
|
29
|
+
}, {
|
|
30
|
+
readonly name: "loop";
|
|
31
|
+
readonly description: "Run a prompt on a recurring interval (/loop 5m <prompt>|list|stop [id])";
|
|
32
|
+
}];
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Slash-command and MCP-server picker overlays, the local /goal & /loop
|
|
4
|
+
* command table, and the shared PalettePicker list UI they render through.
|
|
5
|
+
*/
|
|
6
|
+
import { useEffect, useMemo, useState } from "react";
|
|
7
|
+
import { Box, Text, useInput } from "ink";
|
|
8
|
+
import { registry as slashRegistry } from "../slash-commands/index.js";
|
|
9
|
+
import { isGrokLocalSlashCommand } from "../external-runtime/grok-input-policy.js";
|
|
10
|
+
import { useTheme } from "./theme.js";
|
|
11
|
+
import { isPrintablePickerInput } from "./model-picker.js";
|
|
12
|
+
import { isKeyReleaseEvent } from "./key-events.js";
|
|
13
|
+
import { clampWindowStartForIndex, truncate } from "./app-helpers.js";
|
|
14
|
+
export function buildCommandPaletteItems(skillRegistry, grokSessionBound = false) {
|
|
15
|
+
const items = new Map();
|
|
16
|
+
const add = (item) => {
|
|
17
|
+
const key = `${item.action ?? "command"}:${item.value}`;
|
|
18
|
+
if (!items.has(key))
|
|
19
|
+
items.set(key, item);
|
|
20
|
+
};
|
|
21
|
+
if (!grokSessionBound) {
|
|
22
|
+
for (const command of INK_LOCAL_SLASH_COMMANDS) {
|
|
23
|
+
add({
|
|
24
|
+
label: `/${command.name}`,
|
|
25
|
+
detail: command.description,
|
|
26
|
+
value: command.name,
|
|
27
|
+
command: `/${command.name}`,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
for (const command of slashRegistry.list()) {
|
|
32
|
+
if (grokSessionBound && !isGrokLocalSlashCommand(command.name))
|
|
33
|
+
continue;
|
|
34
|
+
const source = command.source === "mcp" ? " :mcp" : "";
|
|
35
|
+
const sourceLabel = command.sourceLabel ? `[${command.sourceLabel}] ` : "";
|
|
36
|
+
add({
|
|
37
|
+
label: `/${command.name}${source}`,
|
|
38
|
+
detail: `${sourceLabel}${command.description}`,
|
|
39
|
+
value: command.name,
|
|
40
|
+
command: `/${command.name}`,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (!grokSessionBound) {
|
|
44
|
+
for (const skill of skillRegistry.summaries()) {
|
|
45
|
+
add({
|
|
46
|
+
label: `/${skill.name} :skill`,
|
|
47
|
+
detail: `[${skill.source}] ${skill.description}`,
|
|
48
|
+
value: skill.name,
|
|
49
|
+
command: `/${skill.name}`,
|
|
50
|
+
action: "insert-skill",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return [...items.values()];
|
|
55
|
+
}
|
|
56
|
+
export function buildMcpReconnectItems(mcpManager) {
|
|
57
|
+
return (mcpManager?.getStates() ?? []).map((state) => {
|
|
58
|
+
let detail;
|
|
59
|
+
if (state.status.kind === "connected") {
|
|
60
|
+
const tools = state.status.tools.length;
|
|
61
|
+
const prompts = state.status.prompts.length;
|
|
62
|
+
detail = `connected · ${tools} tool${tools === 1 ? "" : "s"} · ${prompts} prompt${prompts === 1 ? "" : "s"}`;
|
|
63
|
+
}
|
|
64
|
+
else if (state.status.kind === "failed") {
|
|
65
|
+
detail = `failed · ${state.status.error}`;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
detail = "disabled";
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
label: state.name,
|
|
72
|
+
detail,
|
|
73
|
+
value: state.name,
|
|
74
|
+
command: `/mcp reconnect ${state.name}`,
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export function CommandPalette({ items, terminalColumns, terminalRows, onSelect, onCancel, }) {
|
|
79
|
+
return (_jsx(PalettePicker, { title: "Commands", hint: "Type to filter \u00B7 Up/Down choose \u00B7 Enter run \u00B7 Esc cancel", emptyText: "No commands found.", items: items, terminalColumns: terminalColumns, terminalRows: terminalRows, searchable: true, onSelect: onSelect, onCancel: onCancel }));
|
|
80
|
+
}
|
|
81
|
+
export function McpReconnectPicker({ items, terminalColumns, terminalRows, onSelect, onCancel, }) {
|
|
82
|
+
return (_jsx(PalettePicker, { title: "MCP servers", hint: "Up/Down choose \u00B7 Enter or r reconnect \u00B7 Esc cancel", emptyText: "No MCP servers configured.", items: items, terminalColumns: terminalColumns, terminalRows: terminalRows, reconnectAlias: true, onSelect: onSelect, onCancel: onCancel }));
|
|
83
|
+
}
|
|
84
|
+
function PalettePicker({ title, hint, emptyText, items, terminalColumns, terminalRows, searchable = false, reconnectAlias = false, onSelect, onCancel, }) {
|
|
85
|
+
const theme = useTheme();
|
|
86
|
+
const [query, setQuery] = useState("");
|
|
87
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
88
|
+
const maxVisible = Math.max(5, Math.min(12, terminalRows - 10));
|
|
89
|
+
const filtered = useMemo(() => {
|
|
90
|
+
const needle = query.trim().toLowerCase();
|
|
91
|
+
if (!needle)
|
|
92
|
+
return items;
|
|
93
|
+
return items.filter((item) => item.label.toLowerCase().includes(needle) ||
|
|
94
|
+
item.detail.toLowerCase().includes(needle) ||
|
|
95
|
+
item.value.toLowerCase().includes(needle));
|
|
96
|
+
}, [items, query]);
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
setSelectedIndex((current) => Math.min(Math.max(0, filtered.length - 1), current));
|
|
99
|
+
}, [filtered.length]);
|
|
100
|
+
useInput((input, key) => {
|
|
101
|
+
if (isKeyReleaseEvent(key))
|
|
102
|
+
return;
|
|
103
|
+
if (key.escape) {
|
|
104
|
+
onCancel();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (key.return || (reconnectAlias && input.toLowerCase() === "r")) {
|
|
108
|
+
const item = filtered[selectedIndex];
|
|
109
|
+
if (item)
|
|
110
|
+
onSelect(item);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (key.upArrow) {
|
|
114
|
+
setSelectedIndex((index) => Math.max(0, index - 1));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (key.downArrow) {
|
|
118
|
+
setSelectedIndex((index) => Math.min(Math.max(0, filtered.length - 1), index + 1));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (key.pageUp) {
|
|
122
|
+
setSelectedIndex((index) => Math.max(0, index - maxVisible));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (key.pageDown) {
|
|
126
|
+
setSelectedIndex((index) => Math.min(Math.max(0, filtered.length - 1), index + maxVisible));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (!searchable)
|
|
130
|
+
return;
|
|
131
|
+
if (key.backspace || key.delete) {
|
|
132
|
+
setQuery((current) => current.slice(0, -1));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (isPrintablePickerInput(input) && !key.ctrl && !key.meta) {
|
|
136
|
+
setQuery((current) => current + input);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
const start = clampWindowStartForIndex(filtered.length, selectedIndex, maxVisible);
|
|
140
|
+
const visible = filtered.slice(start, start + maxVisible);
|
|
141
|
+
const labelWidth = Math.max(18, Math.min(36, Math.floor(terminalColumns * 0.32)));
|
|
142
|
+
const detailWidth = Math.max(20, terminalColumns - labelWidth - 10);
|
|
143
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: title }), searchable && (_jsxs(Text, { color: theme.muted, children: ["Filter: ", _jsx(Text, { color: theme.userMessageText, children: query || " " })] })), _jsx(Text, { color: theme.muted, children: hint }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [filtered.length === 0 && _jsx(Text, { color: theme.muted, children: emptyText }), visible.map((item, offset) => {
|
|
144
|
+
const actualIndex = start + offset;
|
|
145
|
+
const selected = actualIndex === selectedIndex;
|
|
146
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: selected ? theme.accent : undefined, children: [selected ? "> " : " ", truncate(item.label, labelWidth)] }), _jsxs(Text, { color: theme.muted, children: [" ", truncate(item.detail, detailWidth)] })] }, `${item.action ?? "command"}-${item.value}`));
|
|
147
|
+
})] })] }));
|
|
148
|
+
}
|
|
149
|
+
export const INK_LOCAL_SLASH_COMMANDS = [
|
|
150
|
+
{
|
|
151
|
+
name: "goal",
|
|
152
|
+
description: "Set/manage an autonomous goal (/goal <objective>|clear|pause|resume|edit)",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "loop",
|
|
156
|
+
description: "Run a prompt on a recurring interval (/loop 5m <prompt>|list|stop [id])",
|
|
157
|
+
},
|
|
158
|
+
];
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rebuilds UI DisplayMessages from the agent's raw transcript — used on
|
|
3
|
+
* initial mount and after transcript rewrites (rewind, resume).
|
|
4
|
+
*/
|
|
5
|
+
import { INTERRUPTED_ASSISTANT_CONTENT } from "../agent.js";
|
|
6
|
+
import { isHiddenToolMetadata } from "../agent/tool-visibility.js";
|
|
7
|
+
import { isInternalBlockOnlyContent } from "../agent/internal-reminder-sanitizer.js";
|
|
8
|
+
import { nextDisplayMessageKey, stripInterruptedAssistantMarker } from "./display-history.js";
|
|
9
|
+
export function reconstructDisplayMessages(agentMessages) {
|
|
10
|
+
const result = [];
|
|
11
|
+
for (const m of agentMessages) {
|
|
12
|
+
if (m.role === "system" || m.role === "tool")
|
|
13
|
+
continue;
|
|
14
|
+
if (m.role === "user") {
|
|
15
|
+
if (m.isMeta)
|
|
16
|
+
continue; // <system-reminder> injections are not user-visible
|
|
17
|
+
// Harness-initiated kicks (goal continuations, task wakes) persist as
|
|
18
|
+
// user-role messages wrapped in internal blocks so the model keeps
|
|
19
|
+
// them across resume — but they were hidden live and must stay hidden
|
|
20
|
+
// when the transcript is rebuilt.
|
|
21
|
+
if (isInternalBlockOnlyContent(m.content))
|
|
22
|
+
continue;
|
|
23
|
+
result.push({
|
|
24
|
+
key: nextDisplayMessageKey("user"),
|
|
25
|
+
role: "user",
|
|
26
|
+
content: typeof m.content === "string" ? m.content : "(multimedia)",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
else if (m.role === "assistant") {
|
|
30
|
+
const toolCalls = [];
|
|
31
|
+
if (m.toolCalls) {
|
|
32
|
+
for (const tc of m.toolCalls) {
|
|
33
|
+
let args = {};
|
|
34
|
+
try {
|
|
35
|
+
args = JSON.parse(tc.arguments || "{}");
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
args = {};
|
|
39
|
+
}
|
|
40
|
+
const toolResult = agentMessages.find((tm) => tm.role === "tool" && tm.toolCallId === tc.id);
|
|
41
|
+
if (isHiddenToolMetadata(toolResult ? toolResult.metadata : undefined))
|
|
42
|
+
continue;
|
|
43
|
+
toolCalls.push({
|
|
44
|
+
id: tc.id,
|
|
45
|
+
name: tc.name,
|
|
46
|
+
args,
|
|
47
|
+
result: toolResult ? toolResult.content : undefined,
|
|
48
|
+
isError: toolResult ? toolResult.content?.startsWith?.("Error:") : false,
|
|
49
|
+
metadata: toolResult ? toolResult.metadata : undefined,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// An aborted assistant message carries the model-facing interruption
|
|
54
|
+
// note in its content. Render only what the assistant actually said
|
|
55
|
+
// (partial streamed text, if any) plus a dedicated interrupt row —
|
|
56
|
+
// never the note itself, which reads like a leaked system prompt.
|
|
57
|
+
const interrupted = m.error?.aborted === true;
|
|
58
|
+
const content = interrupted
|
|
59
|
+
? stripInterruptedAssistantMarker(m.content, INTERRUPTED_ASSISTANT_CONTENT)
|
|
60
|
+
: m.content;
|
|
61
|
+
if (content || m.reasoning || toolCalls.length > 0 || m.systemFingerprint) {
|
|
62
|
+
result.push({
|
|
63
|
+
key: nextDisplayMessageKey("asst"),
|
|
64
|
+
role: "assistant",
|
|
65
|
+
content,
|
|
66
|
+
reasoning: m.reasoning || undefined,
|
|
67
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
68
|
+
systemFingerprint: m.systemFingerprint,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (interrupted) {
|
|
72
|
+
result.push({
|
|
73
|
+
key: nextDisplayMessageKey("asst"),
|
|
74
|
+
role: "assistant",
|
|
75
|
+
content: "Interrupted by user",
|
|
76
|
+
syntheticKind: "ui_interrupt",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SessionManager } from "../session.js";
|
|
2
|
+
export declare function RewindPicker({ sessionManager, terminalColumns, terminalRows, onSelect, onCancel, }: {
|
|
3
|
+
sessionManager: SessionManager;
|
|
4
|
+
terminalColumns: number;
|
|
5
|
+
terminalRows: number;
|
|
6
|
+
onSelect: (command: string) => void;
|
|
7
|
+
onCancel: () => void;
|
|
8
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* /rewind checkpoint picker: lists user turns with touched-file counts and
|
|
4
|
+
* cycles the restore scope (chat + files / chat only / files only).
|
|
5
|
+
*/
|
|
6
|
+
import { useEffect, useMemo, useState } from "react";
|
|
7
|
+
import { Box, Text, useInput } from "ink";
|
|
8
|
+
import { useTheme } from "./theme.js";
|
|
9
|
+
import { isKeyReleaseEvent } from "./key-events.js";
|
|
10
|
+
import { clampWindowStartForIndex, truncate } from "./app-helpers.js";
|
|
11
|
+
const REWIND_SCOPE_ORDER = ["all", "chat", "code"];
|
|
12
|
+
const REWIND_SCOPE_LABEL = {
|
|
13
|
+
all: "chat + files",
|
|
14
|
+
chat: "chat only",
|
|
15
|
+
code: "files only",
|
|
16
|
+
};
|
|
17
|
+
function rewindCommand(turnIndex, scope) {
|
|
18
|
+
const base = `/rewind ${turnIndex + 1}`;
|
|
19
|
+
if (scope === "chat")
|
|
20
|
+
return `${base} --chat`;
|
|
21
|
+
if (scope === "code")
|
|
22
|
+
return `${base} --code`;
|
|
23
|
+
return base;
|
|
24
|
+
}
|
|
25
|
+
function cycleRewindScope(scope, direction) {
|
|
26
|
+
const index = REWIND_SCOPE_ORDER.indexOf(scope);
|
|
27
|
+
return REWIND_SCOPE_ORDER[(index + direction + REWIND_SCOPE_ORDER.length) % REWIND_SCOPE_ORDER.length];
|
|
28
|
+
}
|
|
29
|
+
export function RewindPicker({ sessionManager, terminalColumns, terminalRows, onSelect, onCancel, }) {
|
|
30
|
+
const theme = useTheme();
|
|
31
|
+
const turns = useMemo(() => sessionManager.listUserTurns(), [sessionManager]);
|
|
32
|
+
const checkpoints = useMemo(() => sessionManager.getCheckpoints(), [sessionManager]);
|
|
33
|
+
const fileCounts = useMemo(() => {
|
|
34
|
+
const entries = checkpoints.listEntries();
|
|
35
|
+
const byTurn = new Map();
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
const files = byTurn.get(entry.turn);
|
|
38
|
+
if (files)
|
|
39
|
+
files.add(entry.path);
|
|
40
|
+
else
|
|
41
|
+
byTurn.set(entry.turn, new Set([entry.path]));
|
|
42
|
+
}
|
|
43
|
+
return new Map(turns.map((turn) => [turn.id, byTurn.get(turn.id)?.size ?? 0]));
|
|
44
|
+
}, [checkpoints, turns]);
|
|
45
|
+
const [selectedIndex, setSelectedIndex] = useState(() => Math.max(0, turns.length - 1));
|
|
46
|
+
const [scope, setScope] = useState("all");
|
|
47
|
+
const maxVisible = Math.max(4, Math.min(10, terminalRows - 10));
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
setSelectedIndex((current) => Math.min(Math.max(0, turns.length - 1), current));
|
|
50
|
+
}, [turns.length]);
|
|
51
|
+
useInput((input, key) => {
|
|
52
|
+
if (isKeyReleaseEvent(key))
|
|
53
|
+
return;
|
|
54
|
+
if (key.escape) {
|
|
55
|
+
onCancel();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (key.return) {
|
|
59
|
+
if (turns[selectedIndex])
|
|
60
|
+
onSelect(rewindCommand(selectedIndex, scope));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (key.upArrow) {
|
|
64
|
+
setSelectedIndex((index) => Math.max(0, index - 1));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (key.downArrow) {
|
|
68
|
+
setSelectedIndex((index) => Math.min(Math.max(0, turns.length - 1), index + 1));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (key.pageUp) {
|
|
72
|
+
setSelectedIndex((index) => Math.max(0, index - maxVisible));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (key.pageDown) {
|
|
76
|
+
setSelectedIndex((index) => Math.min(Math.max(0, turns.length - 1), index + maxVisible));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (key.tab || key.rightArrow || input === "l") {
|
|
80
|
+
setScope((current) => cycleRewindScope(current, 1));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (key.leftArrow || input === "h") {
|
|
84
|
+
setScope((current) => cycleRewindScope(current, -1));
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
const start = clampWindowStartForIndex(turns.length, selectedIndex, maxVisible);
|
|
88
|
+
const visibleTurns = turns.slice(start, start + maxVisible);
|
|
89
|
+
const previewWidth = Math.max(18, Math.min(76, terminalColumns - 34));
|
|
90
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: "Rewind" }), _jsxs(Text, { color: theme.muted, children: ["Restore: ", _jsx(Text, { color: theme.accent, children: REWIND_SCOPE_LABEL[scope] }), " · ", turns.length, " point", turns.length === 1 ? "" : "s"] }), _jsx(Text, { color: theme.muted, children: "Up/Down choose \u00B7 Left/Right scope \u00B7 Enter rewind \u00B7 Esc cancel" }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [turns.length === 0 && _jsx(Text, { color: theme.muted, children: "Nothing to rewind: no user messages in this session." }), visibleTurns.map((turn, offset) => {
|
|
91
|
+
const actualIndex = start + offset;
|
|
92
|
+
const isSelected = actualIndex === selectedIndex;
|
|
93
|
+
const touched = fileCounts.get(turn.id) ?? 0;
|
|
94
|
+
return (_jsx(RewindRow, { turn: turn, turnNumber: actualIndex + 1, selected: isSelected, fileCount: touched, previewWidth: previewWidth }, turn.id));
|
|
95
|
+
})] })] }));
|
|
96
|
+
}
|
|
97
|
+
function RewindRow({ turn, turnNumber, selected, fileCount, previewWidth, }) {
|
|
98
|
+
const theme = useTheme();
|
|
99
|
+
const time = new Date(turn.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
100
|
+
const fileNote = fileCount > 0 ? ` · ${fileCount} file${fileCount === 1 ? "" : "s"}` : "";
|
|
101
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: selected ? theme.accent : undefined, children: [selected ? "> " : " ", String(turnNumber).padStart(2, " "), " ", time, " ", truncate(turn.preview, previewWidth)] }), _jsx(Text, { color: theme.muted, children: fileNote })] }));
|
|
102
|
+
}
|
|
@@ -19,7 +19,7 @@ export function SessionPicker({ currentCwd, currentSessions, allSessions, onSele
|
|
|
19
19
|
? 0
|
|
20
20
|
: Math.min(selectedSessionIdx, sessionRowIndices.length - 1);
|
|
21
21
|
const selectedRowIndex = sessionRowIndices[clampedIdx] ?? -1;
|
|
22
|
-
useInput((
|
|
22
|
+
useInput((_input, key) => {
|
|
23
23
|
if (isKeyReleaseEvent(key))
|
|
24
24
|
return;
|
|
25
25
|
if (key.escape) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type StatsRange, type UsageStatsBundle } from "../stats/usage.js";
|
|
2
|
+
export declare function StatsPanel({ panel, terminalColumns, terminalRows, onRangeChange, onCancel, }: {
|
|
3
|
+
panel: {
|
|
4
|
+
range: StatsRange;
|
|
5
|
+
bundle: UsageStatsBundle;
|
|
6
|
+
};
|
|
7
|
+
terminalColumns: number;
|
|
8
|
+
terminalRows: number;
|
|
9
|
+
onRangeChange: (range: StatsRange) => void;
|
|
10
|
+
onCancel: () => void;
|
|
11
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* /stats overlay: range-switching, scrollable usage panel.
|
|
4
|
+
*/
|
|
5
|
+
import { useEffect, useMemo, useState } from "react";
|
|
6
|
+
import { Box, Text, useInput } from "ink";
|
|
7
|
+
import { formatStatsPanelBody, rangeLabel } from "../stats/usage.js";
|
|
8
|
+
import { useTheme } from "./theme.js";
|
|
9
|
+
import { isKeyReleaseEvent } from "./key-events.js";
|
|
10
|
+
export function StatsPanel({ panel, terminalColumns, terminalRows, onRangeChange, onCancel, }) {
|
|
11
|
+
const theme = useTheme();
|
|
12
|
+
const [scroll, setScroll] = useState(0);
|
|
13
|
+
const bodyWidth = Math.max(48, Math.min(92, terminalColumns - 6));
|
|
14
|
+
const lines = useMemo(() => formatStatsPanelBody(panel.bundle.ranges[panel.range], bodyWidth).split("\n"), [bodyWidth, panel.bundle, panel.range]);
|
|
15
|
+
const maxVisible = Math.max(5, Math.min(16, terminalRows - 10));
|
|
16
|
+
const maxScroll = Math.max(0, lines.length - maxVisible);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
setScroll(0);
|
|
19
|
+
}, [panel.range]);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
setScroll((current) => Math.min(current, maxScroll));
|
|
22
|
+
}, [maxScroll]);
|
|
23
|
+
useInput((input, key) => {
|
|
24
|
+
if (isKeyReleaseEvent(key))
|
|
25
|
+
return;
|
|
26
|
+
if (key.escape) {
|
|
27
|
+
onCancel();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (key.tab) {
|
|
31
|
+
onRangeChange(panel.range === "30d" ? "7d" : "30d");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (key.leftArrow || input === "h") {
|
|
35
|
+
onRangeChange("7d");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (key.rightArrow || input === "l") {
|
|
39
|
+
onRangeChange("30d");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (key.upArrow) {
|
|
43
|
+
setScroll((current) => Math.max(0, current - 1));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (key.downArrow) {
|
|
47
|
+
setScroll((current) => Math.min(maxScroll, current + 1));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (key.pageUp) {
|
|
51
|
+
setScroll((current) => Math.max(0, current - maxVisible));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (key.pageDown) {
|
|
55
|
+
setScroll((current) => Math.min(maxScroll, current + maxVisible));
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
const visible = lines.slice(scroll, scroll + maxVisible);
|
|
59
|
+
const generatedAt = panel.bundle.generatedAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
60
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: "Stats" }), _jsxs(Text, { color: theme.muted, children: [rangeLabel(panel.range), " \u00B7 generated ", generatedAt] }), _jsx(Text, { color: theme.muted, children: "Left/Right range \u00B7 Up/Down scroll \u00B7 Tab toggle \u00B7 Esc close" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: visible.map((line, index) => {
|
|
61
|
+
const key = `${scroll + index}-${line}`;
|
|
62
|
+
const heading = line === "Activity" || line === "Model usage" || line === "Summary";
|
|
63
|
+
return (_jsx(Text, { color: heading ? theme.accent : undefined, bold: heading, children: line || " " }, key));
|
|
64
|
+
}) }), maxScroll > 0 && (_jsxs(Text, { color: theme.muted, children: [scroll + 1, "-", Math.min(lines.length, scroll + maxVisible), " of ", lines.length] }))] }));
|
|
65
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DisplayToolCall } from "./display-history.js";
|
|
2
|
+
interface WaitingIndicatorProps {
|
|
3
|
+
tools: DisplayToolCall[];
|
|
4
|
+
hasStreamingText: boolean;
|
|
5
|
+
hasStreamingReasoning: boolean;
|
|
6
|
+
streamedChars: number;
|
|
7
|
+
nowTick: number;
|
|
8
|
+
pendingSteerCount?: number;
|
|
9
|
+
queuedCount?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function WaitingIndicator({ tools, hasStreamingText, hasStreamingReasoning, streamedChars, nowTick, pendingSteerCount, queuedCount, }: WaitingIndicatorProps): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The animated spinner row shown while a run is in flight: tool-targeted or
|
|
4
|
+
* generic phrases, streaming token counter, and steer/queue hints.
|
|
5
|
+
*/
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
import { Box, Text } from "ink";
|
|
8
|
+
import { useTheme } from "./theme.js";
|
|
9
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
10
|
+
const GENERIC_PHRASES = [
|
|
11
|
+
"mapping the workspace",
|
|
12
|
+
"reading the room",
|
|
13
|
+
"following the threads",
|
|
14
|
+
"connecting the pieces",
|
|
15
|
+
"sorting the context",
|
|
16
|
+
"scanning the structure",
|
|
17
|
+
"shaping the next step",
|
|
18
|
+
"gathering signal",
|
|
19
|
+
"checking the edges",
|
|
20
|
+
"lining up the answer",
|
|
21
|
+
"tracing the flow",
|
|
22
|
+
"building the picture",
|
|
23
|
+
"walking the graph",
|
|
24
|
+
"collecting the clues",
|
|
25
|
+
"framing the problem",
|
|
26
|
+
"locating the source",
|
|
27
|
+
"resolving the shape",
|
|
28
|
+
"untangling the state",
|
|
29
|
+
"comparing the paths",
|
|
30
|
+
"narrowing the target",
|
|
31
|
+
"tracking the changes",
|
|
32
|
+
"reading the patterns",
|
|
33
|
+
"weighing the options",
|
|
34
|
+
"assembling the context",
|
|
35
|
+
"following the signal",
|
|
36
|
+
"checking the assumptions",
|
|
37
|
+
"aligning the details",
|
|
38
|
+
"testing the shape",
|
|
39
|
+
"pulling the thread",
|
|
40
|
+
"cleaning the edges",
|
|
41
|
+
"refining the draft",
|
|
42
|
+
"verifying the route",
|
|
43
|
+
"making sense of it",
|
|
44
|
+
"looking for leverage",
|
|
45
|
+
"stitching the answer",
|
|
46
|
+
"holding the thread",
|
|
47
|
+
"distilling the noise",
|
|
48
|
+
"finding the seam",
|
|
49
|
+
"reading between the lines",
|
|
50
|
+
"preparing the response",
|
|
51
|
+
];
|
|
52
|
+
const TOOL_TARGET_PHRASES = {
|
|
53
|
+
read: "reading files",
|
|
54
|
+
write: "writing changes",
|
|
55
|
+
edit: "patching files",
|
|
56
|
+
grep: "searching the codebase",
|
|
57
|
+
glob: "scanning paths",
|
|
58
|
+
ls: "listing directories",
|
|
59
|
+
bash: "running command",
|
|
60
|
+
web_search: "searching the web",
|
|
61
|
+
web_fetch: "fetching a page",
|
|
62
|
+
task: "spawning subagent",
|
|
63
|
+
};
|
|
64
|
+
function formatTokensApprox(chars) {
|
|
65
|
+
const tokens = Math.max(0, Math.round(chars / 4));
|
|
66
|
+
if (tokens < 1000)
|
|
67
|
+
return `${tokens}`;
|
|
68
|
+
if (tokens < 10000)
|
|
69
|
+
return `${(tokens / 1000).toFixed(1)}k`;
|
|
70
|
+
return `${Math.round(tokens / 1000)}k`;
|
|
71
|
+
}
|
|
72
|
+
export function WaitingIndicator({ tools, hasStreamingText, hasStreamingReasoning, streamedChars, nowTick, pendingSteerCount = 0, queuedCount = 0, }) {
|
|
73
|
+
void nowTick;
|
|
74
|
+
const theme = useTheme();
|
|
75
|
+
const [frameIndex, setFrameIndex] = useState(0);
|
|
76
|
+
const [idlePhrase, setIdlePhrase] = useState(() => GENERIC_PHRASES[0]);
|
|
77
|
+
// Frame timer is independent of the agent state — keeps animation smooth.
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const t = setInterval(() => {
|
|
80
|
+
setFrameIndex((i) => (i + 1) % SPINNER_FRAMES.length);
|
|
81
|
+
}, 100);
|
|
82
|
+
return () => clearInterval(t);
|
|
83
|
+
}, []);
|
|
84
|
+
// Determine state: active tool > streaming text > streaming reasoning > idle
|
|
85
|
+
const activeTool = [...tools].reverse().find((t) => !t.result);
|
|
86
|
+
const state = activeTool
|
|
87
|
+
? "tool"
|
|
88
|
+
: hasStreamingText
|
|
89
|
+
? "text"
|
|
90
|
+
: hasStreamingReasoning
|
|
91
|
+
? "reasoning"
|
|
92
|
+
: "idle";
|
|
93
|
+
// Rotate idle phrases on a slower cadence; only matters in the idle state.
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (state !== "idle")
|
|
96
|
+
return;
|
|
97
|
+
const t = setInterval(() => {
|
|
98
|
+
setIdlePhrase((current) => {
|
|
99
|
+
const candidates = GENERIC_PHRASES.filter((item) => item !== current);
|
|
100
|
+
return candidates[Math.floor(Math.random() * candidates.length)] || current;
|
|
101
|
+
});
|
|
102
|
+
}, 1500);
|
|
103
|
+
return () => clearInterval(t);
|
|
104
|
+
}, [state]);
|
|
105
|
+
let phrase;
|
|
106
|
+
if (state === "tool" && activeTool) {
|
|
107
|
+
phrase =
|
|
108
|
+
TOOL_TARGET_PHRASES[activeTool.name] || `running ${activeTool.name}`;
|
|
109
|
+
}
|
|
110
|
+
else if (state === "text") {
|
|
111
|
+
phrase = "writing the response";
|
|
112
|
+
}
|
|
113
|
+
else if (state === "reasoning") {
|
|
114
|
+
phrase = "working through the request";
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
phrase = idlePhrase;
|
|
118
|
+
}
|
|
119
|
+
const tokenText = streamedChars > 0 ? `↓${formatTokensApprox(streamedChars)} tok` : "";
|
|
120
|
+
const hintParts = [];
|
|
121
|
+
if (tokenText)
|
|
122
|
+
hintParts.push(tokenText);
|
|
123
|
+
if (pendingSteerCount > 0)
|
|
124
|
+
hintParts.push(`${pendingSteerCount} pending steer${pendingSteerCount === 1 ? "" : "s"}`);
|
|
125
|
+
if (queuedCount > 0)
|
|
126
|
+
hintParts.push(`${queuedCount} queued`);
|
|
127
|
+
hintParts.push("enter steer", "tab queue", "esc stop");
|
|
128
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, children: SPINNER_FRAMES[frameIndex] }), _jsxs(Text, { color: theme.muted, children: [" ", phrase, " "] }), _jsxs(Text, { color: theme.muted, dimColor: true, children: ["(", hintParts.join(" · "), ")"] })] }));
|
|
129
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -284,12 +284,6 @@ export type PlanDecision = {
|
|
|
284
284
|
action: "reject";
|
|
285
285
|
reason?: string;
|
|
286
286
|
};
|
|
287
|
-
export type TodoStatus = "pending" | "in_progress" | "completed";
|
|
288
|
-
export interface Todo {
|
|
289
|
-
content: string;
|
|
290
|
-
status: TodoStatus;
|
|
291
|
-
activeForm: string;
|
|
292
|
-
}
|
|
293
287
|
export type StreamChunk = {
|
|
294
288
|
type: "text";
|
|
295
289
|
content: string;
|
|
@@ -463,9 +457,6 @@ export type AgentEvent = {
|
|
|
463
457
|
} | {
|
|
464
458
|
type: "mode_changed";
|
|
465
459
|
mode: PermissionMode;
|
|
466
|
-
} | {
|
|
467
|
-
type: "todos_updated";
|
|
468
|
-
todos: Todo[];
|
|
469
460
|
} | {
|
|
470
461
|
type: "agent_end";
|
|
471
462
|
};
|