@bubblebrain-ai/bubble 0.0.48 → 0.0.50
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/README.md +17 -0
- package/dist/agent/abort-errors.d.ts +4 -0
- package/dist/agent/abort-errors.js +30 -0
- 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/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 -16
- package/dist/agent.js +90 -300
- package/dist/external-runtime/grok-runtime.js +1 -1
- package/dist/feishu/agent-host/run-driver.js +1 -1
- package/dist/lsp/service.js +2 -2
- package/dist/model-catalog.d.ts +13 -0
- package/dist/model-catalog.js +52 -8
- package/dist/permissions/settings.d.ts +0 -1
- package/dist/permissions/settings.js +0 -2
- package/dist/provider-grok.d.ts +16 -18
- package/dist/provider-grok.js +28 -20
- package/dist/provider-registry.d.ts +9 -0
- package/dist/provider-registry.js +109 -6
- package/dist/provider-transform.js +7 -8
- package/dist/provider.js +11 -0
- package/dist/session-log.js +1 -0
- package/dist/slash-commands/commands.js +13 -9
- package/dist/tools/grep.js +1 -1
- 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 +104 -635
- package/dist/tui-ink/command-palette.d.ts +32 -0
- package/dist/tui-ink/command-palette.js +158 -0
- package/dist/tui-ink/display-history.d.ts +2 -0
- package/dist/tui-ink/display-reconstruct.d.ts +3 -0
- package/dist/tui-ink/display-reconstruct.js +82 -0
- package/dist/tui-ink/footer.d.ts +14 -3
- package/dist/tui-ink/footer.js +19 -5
- 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/tui-ink/wordmark.d.ts +8 -0
- package/dist/tui-ink/wordmark.js +103 -0
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
|
@@ -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
|
+
];
|
|
@@ -17,6 +17,8 @@ export interface DisplayMessage {
|
|
|
17
17
|
compactionSummary?: string;
|
|
18
18
|
hiddenCount?: number;
|
|
19
19
|
taskElapsedMs?: number;
|
|
20
|
+
/** Backend configuration fingerprint reported for this model response. */
|
|
21
|
+
systemFingerprint?: string;
|
|
20
22
|
}
|
|
21
23
|
export declare function userInputStatusBadgeLabel(status?: UserInputStatus): string | undefined;
|
|
22
24
|
export declare function setUserInputStatus(message: DisplayMessage, inputStatus?: UserInputStatus): DisplayMessage;
|
|
@@ -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
|
+
}
|
package/dist/tui-ink/footer.d.ts
CHANGED
|
@@ -3,11 +3,22 @@ export interface FooterData {
|
|
|
3
3
|
mode?: PermissionMode;
|
|
4
4
|
goalLine?: string;
|
|
5
5
|
runtimeLabel?: string;
|
|
6
|
+
/** Friendly working directory (e.g. `~/project`). */
|
|
7
|
+
cwd?: string;
|
|
8
|
+
/** Current git branch, when the cwd is a git repo. */
|
|
9
|
+
branch?: string;
|
|
10
|
+
/** Display name of the active model. */
|
|
11
|
+
model?: string;
|
|
12
|
+
/** Current session title, when one has been assigned. */
|
|
13
|
+
sessionTitle?: string;
|
|
14
|
+
/** Compact context usage, e.g. `42K/500K`. */
|
|
15
|
+
contextUsage?: string;
|
|
6
16
|
}
|
|
7
17
|
/**
|
|
8
|
-
* Bottom status line.
|
|
9
|
-
*
|
|
10
|
-
* permission-mode badge
|
|
18
|
+
* Bottom status line. Always surfaces a `cwd | branch | model | title | ctx`
|
|
19
|
+
* row (each segment only when its data is present), and appends the
|
|
20
|
+
* permission-mode badge for non-default modes. Renders nothing (zero rows)
|
|
21
|
+
* when no data is available.
|
|
11
22
|
*/
|
|
12
23
|
export declare function FooterBar({ data }: {
|
|
13
24
|
data: FooterData;
|
package/dist/tui-ink/footer.js
CHANGED
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
2
3
|
import { Box, Text } from "ink";
|
|
3
4
|
import { useTheme } from "./theme.js";
|
|
4
5
|
import { PERMISSION_MODE_INFO } from "../permission/mode.js";
|
|
5
6
|
/**
|
|
6
|
-
* Bottom status line.
|
|
7
|
-
*
|
|
8
|
-
* permission-mode badge
|
|
7
|
+
* Bottom status line. Always surfaces a `cwd | branch | model | title | ctx`
|
|
8
|
+
* row (each segment only when its data is present), and appends the
|
|
9
|
+
* permission-mode badge for non-default modes. Renders nothing (zero rows)
|
|
10
|
+
* when no data is available.
|
|
9
11
|
*/
|
|
10
12
|
export function FooterBar({ data }) {
|
|
13
|
+
const theme = useTheme();
|
|
11
14
|
const showMode = !!data.mode && data.mode !== "default";
|
|
12
15
|
const goalLine = data.goalLine?.trim();
|
|
13
16
|
const runtimeLabel = data.runtimeLabel?.trim();
|
|
14
|
-
|
|
17
|
+
const segments = [];
|
|
18
|
+
if (data.cwd?.trim())
|
|
19
|
+
segments.push({ key: "cwd", text: data.cwd.trim(), color: theme.muted });
|
|
20
|
+
if (data.branch?.trim())
|
|
21
|
+
segments.push({ key: "branch", text: data.branch.trim(), color: theme.muted });
|
|
22
|
+
if (data.model?.trim())
|
|
23
|
+
segments.push({ key: "model", text: data.model.trim(), color: theme.accent });
|
|
24
|
+
if (data.sessionTitle?.trim())
|
|
25
|
+
segments.push({ key: "title", text: data.sessionTitle.trim(), color: theme.muted });
|
|
26
|
+
if (data.contextUsage?.trim())
|
|
27
|
+
segments.push({ key: "ctx", text: data.contextUsage.trim(), color: theme.muted });
|
|
28
|
+
if (!showMode && !goalLine && !runtimeLabel && segments.length === 0)
|
|
15
29
|
return null;
|
|
16
|
-
return (_jsxs(Box, { paddingX: 1, flexShrink: 0, flexDirection: "column", children: [goalLine && _jsx(GoalBadge, { line: goalLine }), runtimeLabel && _jsx(RuntimeBadge, { label: runtimeLabel }), showMode && _jsx(ModeBadge, { mode: data.mode })] }));
|
|
30
|
+
return (_jsxs(Box, { paddingX: 1, flexShrink: 0, flexDirection: "column", children: [goalLine && _jsx(GoalBadge, { line: goalLine }), runtimeLabel && _jsx(RuntimeBadge, { label: runtimeLabel }), (segments.length > 0 || showMode) && (_jsxs(Box, { children: [showMode && (_jsxs(React.Fragment, { children: [_jsx(ModeBadge, { mode: data.mode }), segments.length > 0 && _jsx(Text, { color: theme.muted, children: " | " })] })), segments.map((segment, index) => (_jsxs(React.Fragment, { children: [index > 0 && _jsx(Text, { color: theme.muted, children: " | " }), _jsx(Text, { color: segment.color, children: segment.text })] }, segment.key)))] }))] }));
|
|
17
31
|
}
|
|
18
32
|
function RuntimeBadge({ label }) {
|
|
19
33
|
const theme = useTheme();
|
|
@@ -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 {};
|