@bubblebrain-ai/bubble 0.0.11 → 0.0.13
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/input-controller.d.ts +11 -0
- package/dist/agent/input-controller.js +30 -0
- package/dist/agent.d.ts +6 -4
- package/dist/agent.js +39 -2
- package/dist/feishu/agent-host/run-driver.js +13 -6
- package/dist/feishu/agent-host/runtime-deps.d.ts +2 -2
- package/dist/feishu/router/commands.js +2 -1
- package/dist/feishu/scope/session-binder.js +1 -1
- package/dist/feishu/serve.js +3 -3
- package/dist/main.js +78 -12
- 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 +5 -0
- package/dist/session.js +75 -9
- 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 +29 -22
- package/dist/slash-commands/registry.js +1 -1
- package/dist/slash-commands/types.d.ts +10 -0
- 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/clipboard.d.ts +1 -0
- package/dist/tui/clipboard.js +53 -0
- package/dist/tui/detect-theme.d.ts +2 -0
- package/dist/tui/detect-theme.js +87 -0
- package/dist/tui/display-history.d.ts +62 -0
- package/dist/tui/display-history.js +305 -0
- package/dist/tui/edit-diff.d.ts +11 -0
- package/dist/tui/edit-diff.js +52 -0
- package/dist/tui/escape-confirmation.d.ts +15 -0
- package/dist/tui/escape-confirmation.js +30 -0
- package/dist/tui/file-mentions.d.ts +29 -0
- package/dist/tui/file-mentions.js +174 -0
- package/dist/tui/global-key-router.d.ts +3 -0
- package/dist/tui/global-key-router.js +87 -0
- package/dist/tui/image-paste.d.ts +95 -0
- package/dist/tui/image-paste.js +505 -0
- package/dist/tui/input-history.d.ts +16 -0
- package/dist/tui/input-history.js +79 -0
- package/dist/tui/markdown-inline.d.ts +22 -0
- package/dist/tui/markdown-inline.js +68 -0
- package/dist/tui/markdown-theme-rules.d.ts +23 -0
- package/dist/tui/markdown-theme-rules.js +164 -0
- package/dist/tui/markdown-theme.d.ts +5 -0
- package/dist/tui/markdown-theme.js +27 -0
- package/dist/tui/opencode-spinner.d.ts +22 -0
- package/dist/tui/opencode-spinner.js +216 -0
- package/dist/tui/prompt-keybindings.d.ts +42 -0
- package/dist/tui/prompt-keybindings.js +35 -0
- package/dist/tui/recent-activity.d.ts +8 -0
- package/dist/tui/recent-activity.js +71 -0
- package/dist/tui/render-signature.d.ts +1 -0
- package/dist/tui/render-signature.js +7 -0
- package/dist/tui/run.d.ts +45 -0
- package/dist/tui/run.js +8816 -0
- package/dist/tui/session-display.d.ts +6 -0
- package/dist/tui/session-display.js +12 -0
- package/dist/tui/sidebar-mcp.d.ts +31 -0
- package/dist/tui/sidebar-mcp.js +62 -0
- package/dist/tui/sidebar-state.d.ts +12 -0
- package/dist/tui/sidebar-state.js +69 -0
- package/dist/tui/streaming-tool-args.d.ts +15 -0
- package/dist/tui/streaming-tool-args.js +30 -0
- package/dist/tui/tool-renderers/fallback.d.ts +2 -0
- package/dist/tui/tool-renderers/fallback.js +75 -0
- package/dist/tui/tool-renderers/registry.d.ts +3 -0
- package/dist/tui/tool-renderers/registry.js +11 -0
- package/dist/tui/tool-renderers/subagent.d.ts +2 -0
- package/dist/tui/tool-renderers/subagent.js +135 -0
- package/dist/tui/tool-renderers/types.d.ts +36 -0
- package/dist/tui/tool-renderers/types.js +1 -0
- package/dist/tui/tool-renderers/write-preview.d.ts +12 -0
- package/dist/tui/tool-renderers/write-preview.js +30 -0
- package/dist/tui/tool-renderers/write.d.ts +6 -0
- package/dist/tui/tool-renderers/write.js +88 -0
- package/dist/tui/trace-groups.d.ts +27 -0
- package/dist/tui/trace-groups.js +412 -0
- package/dist/tui/wordmark.d.ts +15 -0
- package/dist/tui/wordmark.js +179 -0
- package/dist/tui-ink/app.js +98 -70
- package/dist/tui-ink/input-box.d.ts +22 -1
- package/dist/tui-ink/input-box.js +105 -11
- package/dist/tui-ink/message-list.js +12 -3
- package/dist/tui-ink/model-picker.d.ts +18 -0
- package/dist/tui-ink/model-picker.js +80 -23
- package/dist/tui-ink/session-picker.js +5 -7
- package/dist/tui-ink/theme.d.ts +3 -9
- package/dist/tui-ink/theme.js +39 -45
- package/dist/tui-ink/welcome.js +22 -78
- package/dist/tui-opentui/app.d.ts +54 -0
- package/dist/tui-opentui/app.js +1363 -0
- package/dist/tui-opentui/approval/approval-dialog.d.ts +15 -0
- package/dist/tui-opentui/approval/approval-dialog.js +139 -0
- package/dist/tui-opentui/approval/diff-view.d.ts +9 -0
- package/dist/tui-opentui/approval/diff-view.js +43 -0
- package/dist/tui-opentui/approval/select.d.ts +37 -0
- package/dist/tui-opentui/approval/select.js +91 -0
- package/dist/tui-opentui/detect-theme.d.ts +2 -0
- package/dist/tui-opentui/detect-theme.js +87 -0
- package/dist/tui-opentui/display-history.d.ts +55 -0
- package/dist/tui-opentui/display-history.js +129 -0
- package/dist/tui-opentui/edit-diff.d.ts +11 -0
- package/dist/tui-opentui/edit-diff.js +52 -0
- package/dist/tui-opentui/feedback-dialog.d.ts +21 -0
- package/dist/tui-opentui/feedback-dialog.js +164 -0
- package/dist/tui-opentui/feishu-setup-picker.d.ts +7 -0
- package/dist/tui-opentui/feishu-setup-picker.js +272 -0
- package/dist/tui-opentui/file-mentions.d.ts +29 -0
- package/dist/tui-opentui/file-mentions.js +174 -0
- package/dist/tui-opentui/footer.d.ts +26 -0
- package/dist/tui-opentui/footer.js +40 -0
- package/dist/tui-opentui/image-paste.d.ts +54 -0
- package/dist/tui-opentui/image-paste.js +288 -0
- package/dist/tui-opentui/input-box.d.ts +34 -0
- package/dist/tui-opentui/input-box.js +471 -0
- package/dist/tui-opentui/input-history.d.ts +16 -0
- package/dist/tui-opentui/input-history.js +79 -0
- package/dist/tui-opentui/markdown.d.ts +66 -0
- package/dist/tui-opentui/markdown.js +127 -0
- package/dist/tui-opentui/message-list.d.ts +31 -0
- package/dist/tui-opentui/message-list.js +125 -0
- package/dist/tui-opentui/model-picker.d.ts +63 -0
- package/dist/tui-opentui/model-picker.js +450 -0
- package/dist/tui-opentui/plan-confirm.d.ts +9 -0
- package/dist/tui-opentui/plan-confirm.js +124 -0
- package/dist/tui-opentui/question-dialog.d.ts +10 -0
- package/dist/tui-opentui/question-dialog.js +110 -0
- package/dist/tui-opentui/recent-activity.d.ts +8 -0
- package/dist/tui-opentui/recent-activity.js +71 -0
- package/dist/tui-opentui/run-session-picker.d.ts +10 -0
- package/dist/tui-opentui/run-session-picker.js +28 -0
- package/dist/tui-opentui/run.d.ts +38 -0
- package/dist/tui-opentui/run.js +48 -0
- package/dist/tui-opentui/session-picker.d.ts +12 -0
- package/dist/tui-opentui/session-picker.js +120 -0
- package/dist/tui-opentui/theme.d.ts +89 -0
- package/dist/tui-opentui/theme.js +157 -0
- package/dist/tui-opentui/todos.d.ts +9 -0
- package/dist/tui-opentui/todos.js +45 -0
- package/dist/tui-opentui/trace-groups.d.ts +27 -0
- package/dist/tui-opentui/trace-groups.js +412 -0
- package/dist/tui-opentui/use-terminal-size.d.ts +4 -0
- package/dist/tui-opentui/use-terminal-size.js +5 -0
- package/dist/tui-opentui/welcome.d.ts +25 -0
- package/dist/tui-opentui/welcome.js +77 -0
- package/dist/types.d.ts +24 -0
- package/package.json +5 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
/** @jsxImportSource @opentui/react */
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
|
+
import { useKeyboard } from "@opentui/react";
|
|
5
|
+
import { useTheme } from "./theme.js";
|
|
6
|
+
export function QuestionDialog({ request, onSubmit, onCancel }) {
|
|
7
|
+
const theme = useTheme();
|
|
8
|
+
const [index, setIndex] = useState(0);
|
|
9
|
+
const [selected, setSelected] = useState(0);
|
|
10
|
+
const [custom, setCustom] = useState("");
|
|
11
|
+
const [answers, setAnswers] = useState(() => request.questions.map(() => []));
|
|
12
|
+
const question = request.questions[index];
|
|
13
|
+
const options = question?.options ?? [];
|
|
14
|
+
const canUseCustom = question?.custom !== false;
|
|
15
|
+
const isMultiple = question?.multiple === true;
|
|
16
|
+
const totalTabs = request.questions.length;
|
|
17
|
+
const currentAnswer = useMemo(() => answers[index] ?? [], [answers, index]);
|
|
18
|
+
const commitQuestion = () => {
|
|
19
|
+
const option = options[selected]?.label;
|
|
20
|
+
const customAnswer = custom.trim();
|
|
21
|
+
const nextAnswer = customAnswer
|
|
22
|
+
? [customAnswer]
|
|
23
|
+
: isMultiple
|
|
24
|
+
? currentAnswer
|
|
25
|
+
: option
|
|
26
|
+
? [option]
|
|
27
|
+
: [];
|
|
28
|
+
const nextAnswers = answers.map((answer, i) => i === index ? nextAnswer : answer);
|
|
29
|
+
if (index < request.questions.length - 1) {
|
|
30
|
+
setAnswers(nextAnswers);
|
|
31
|
+
setIndex((i) => i + 1);
|
|
32
|
+
setSelected(0);
|
|
33
|
+
setCustom("");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
onSubmit(nextAnswers);
|
|
37
|
+
};
|
|
38
|
+
const toggleCurrentOption = () => {
|
|
39
|
+
const option = options[selected]?.label;
|
|
40
|
+
if (!option)
|
|
41
|
+
return;
|
|
42
|
+
if (!isMultiple) {
|
|
43
|
+
setAnswers((prev) => prev.map((answer, i) => i === index ? [option] : answer));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
setAnswers((prev) => prev.map((answer, i) => {
|
|
47
|
+
if (i !== index)
|
|
48
|
+
return answer;
|
|
49
|
+
return answer.includes(option)
|
|
50
|
+
? answer.filter((item) => item !== option)
|
|
51
|
+
: [...answer, option];
|
|
52
|
+
}));
|
|
53
|
+
};
|
|
54
|
+
useKeyboard((key) => {
|
|
55
|
+
if (key.eventType === "release")
|
|
56
|
+
return;
|
|
57
|
+
if (key.name === "escape") {
|
|
58
|
+
onCancel();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (key.name === "left" && index > 0) {
|
|
62
|
+
setIndex((i) => i - 1);
|
|
63
|
+
setSelected(0);
|
|
64
|
+
setCustom("");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (key.name === "right" && index < totalTabs - 1) {
|
|
68
|
+
setIndex((i) => i + 1);
|
|
69
|
+
setSelected(0);
|
|
70
|
+
setCustom("");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (key.name === "up") {
|
|
74
|
+
setSelected((i) => Math.max(0, i - 1));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (key.name === "down") {
|
|
78
|
+
setSelected((i) => Math.min(Math.max(0, options.length - 1), i + 1));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (key.name === "tab" || key.name === " " || key.name === "space") {
|
|
82
|
+
toggleCurrentOption();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (key.name === "return") {
|
|
86
|
+
commitQuestion();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (key.name === "backspace" || key.name === "delete") {
|
|
90
|
+
setCustom((value) => value.slice(0, -1));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (canUseCustom && key.name && key.name.length === 1 && !key.ctrl && !key.option) {
|
|
94
|
+
setCustom((value) => value + key.name);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
return (_jsxs("box", { style: {
|
|
98
|
+
flexDirection: "column",
|
|
99
|
+
border: true,
|
|
100
|
+
borderColor: theme.accent,
|
|
101
|
+
paddingLeft: 1,
|
|
102
|
+
paddingRight: 1,
|
|
103
|
+
marginTop: 1,
|
|
104
|
+
marginBottom: 1,
|
|
105
|
+
}, children: [_jsx("text", { fg: theme.accent, attributes: 1, children: `Question ${totalTabs > 1 ? `${index + 1}/${totalTabs}` : ""}` }), _jsx("box", { style: { marginTop: 1 }, children: _jsx("text", { children: question?.question ?? "The agent is asking for input." }) }), _jsx("box", { style: { flexDirection: "column", marginTop: 1 }, children: options.map((option, optionIndex) => {
|
|
106
|
+
const isSelected = optionIndex === selected;
|
|
107
|
+
const isChecked = currentAnswer.includes(option.label);
|
|
108
|
+
return (_jsxs("box", { style: { flexDirection: "column" }, children: [_jsxs("text", { fg: isSelected ? theme.accent : undefined, children: [isSelected ? "> " : " ", isMultiple ? `[${isChecked ? "x" : " "}] ` : "", option.label] }), option.description && (_jsx("box", { style: { marginLeft: 4 }, children: _jsx("text", { fg: theme.muted, children: option.description }) }))] }, `${option.label}-${optionIndex}`));
|
|
109
|
+
}) }), canUseCustom && (_jsx("box", { style: { marginTop: 1 }, children: _jsx("text", { fg: custom ? undefined : theme.muted, children: `Custom: ${custom || "type to answer..."}` }) })), _jsx("box", { style: { marginTop: 1 }, children: _jsx("text", { fg: theme.muted, children: "\u2191\u2193 choose \u00B7 Tab/Space toggle \u00B7 Enter submit \u00B7 Esc dismiss" }) })] }));
|
|
110
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface RecentSession {
|
|
2
|
+
file: string;
|
|
3
|
+
modifiedAt: number;
|
|
4
|
+
preview: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function getRecentSessions(cwd: string, limit?: number): RecentSession[];
|
|
7
|
+
export declare function formatRelativeTime(timestampMs: number, now?: number): string;
|
|
8
|
+
export declare function truncatePreview(preview: string, maxLen: number): string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync, existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getSessionsDir } from "../session.js";
|
|
4
|
+
export function getRecentSessions(cwd, limit = 3) {
|
|
5
|
+
const dir = getSessionsDir(cwd);
|
|
6
|
+
if (!existsSync(dir))
|
|
7
|
+
return [];
|
|
8
|
+
const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
9
|
+
const withMtime = files.map((f) => {
|
|
10
|
+
const full = path.join(dir, f);
|
|
11
|
+
try {
|
|
12
|
+
return { file: f, full, modifiedAt: statSync(full).mtimeMs };
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return { file: f, full, modifiedAt: 0 };
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
withMtime.sort((a, b) => b.modifiedAt - a.modifiedAt);
|
|
19
|
+
return withMtime.slice(0, limit).map(({ file, full, modifiedAt }) => ({
|
|
20
|
+
file,
|
|
21
|
+
modifiedAt,
|
|
22
|
+
preview: extractFirstUserMessage(full) ?? "(no messages)",
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
export function formatRelativeTime(timestampMs, now = Date.now()) {
|
|
26
|
+
const diffSec = Math.max(0, Math.floor((now - timestampMs) / 1000));
|
|
27
|
+
if (diffSec < 60)
|
|
28
|
+
return "just now";
|
|
29
|
+
if (diffSec < 3600)
|
|
30
|
+
return `${Math.floor(diffSec / 60)}m ago`;
|
|
31
|
+
if (diffSec < 86400)
|
|
32
|
+
return `${Math.floor(diffSec / 3600)}h ago`;
|
|
33
|
+
if (diffSec < 604800)
|
|
34
|
+
return `${Math.floor(diffSec / 86400)}d ago`;
|
|
35
|
+
const weeks = Math.floor(diffSec / 604800);
|
|
36
|
+
if (weeks < 5)
|
|
37
|
+
return `${weeks}w ago`;
|
|
38
|
+
const months = Math.floor(diffSec / (30 * 86400));
|
|
39
|
+
return `${months}mo ago`;
|
|
40
|
+
}
|
|
41
|
+
function extractFirstUserMessage(file) {
|
|
42
|
+
let raw;
|
|
43
|
+
try {
|
|
44
|
+
raw = readFileSync(file, "utf-8");
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const lines = raw.split("\n");
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
if (!line.trim())
|
|
52
|
+
continue;
|
|
53
|
+
let entry;
|
|
54
|
+
try {
|
|
55
|
+
entry = JSON.parse(line);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (entry.type === "user_message" && typeof entry.message?.content === "string") {
|
|
61
|
+
return entry.message.content;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
export function truncatePreview(preview, maxLen) {
|
|
67
|
+
const firstLine = preview.split("\n")[0]?.trim() ?? "";
|
|
68
|
+
if (firstLine.length <= maxLen)
|
|
69
|
+
return firstLine;
|
|
70
|
+
return firstLine.slice(0, Math.max(1, maxLen - 1)) + "…";
|
|
71
|
+
}
|
|
@@ -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,28 @@
|
|
|
1
|
+
import { jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { createCliRenderer } from "@opentui/core";
|
|
3
|
+
import { createRoot } from "@opentui/react";
|
|
4
|
+
import { SessionPicker } from "./session-picker.js";
|
|
5
|
+
import { ThemeProvider, paletteFor } from "./theme.js";
|
|
6
|
+
export async function runSessionPicker(options) {
|
|
7
|
+
const theme = paletteFor(options.resolvedTheme, options.themeOverrides);
|
|
8
|
+
const renderer = await createCliRenderer();
|
|
9
|
+
const root = createRoot(renderer);
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
let done = false;
|
|
12
|
+
const finish = (value) => {
|
|
13
|
+
if (done)
|
|
14
|
+
return;
|
|
15
|
+
done = true;
|
|
16
|
+
try {
|
|
17
|
+
root.unmount();
|
|
18
|
+
}
|
|
19
|
+
catch { /* ignore */ }
|
|
20
|
+
try {
|
|
21
|
+
renderer.destroy();
|
|
22
|
+
}
|
|
23
|
+
catch { /* ignore */ }
|
|
24
|
+
resolve(value);
|
|
25
|
+
};
|
|
26
|
+
root.render(_jsx(ThemeProvider, { value: theme, children: _jsx(SessionPicker, { currentCwd: options.currentCwd, currentSessions: options.currentSessions, allSessions: options.allSessions, onSelect: (file) => finish(file), onCancel: () => finish(undefined) }) }));
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/react */
|
|
2
|
+
import type { Agent } from "../agent.js";
|
|
3
|
+
import type { CliArgs } from "../cli.js";
|
|
4
|
+
import type { SessionManager } from "../session.js";
|
|
5
|
+
import type { Provider } from "../types.js";
|
|
6
|
+
import type { ProviderRegistry } from "../provider-registry.js";
|
|
7
|
+
import type { SkillRegistry } from "../skills/registry.js";
|
|
8
|
+
import { type ApprovalHandlerRef, type PlanHandlerRef } from "./app.js";
|
|
9
|
+
import type { BashAllowlist } from "../approval/session-cache.js";
|
|
10
|
+
import type { SettingsManager } from "../permissions/settings.js";
|
|
11
|
+
import type { McpManager } from "../mcp/manager.js";
|
|
12
|
+
import type { LspService } from "../lsp/index.js";
|
|
13
|
+
import type { QuestionController } from "../question/index.js";
|
|
14
|
+
import type { MemoryScope } from "../memory/index.js";
|
|
15
|
+
import type { ResolvedTheme, ThemeMode } from "./theme.js";
|
|
16
|
+
export interface RunTuiOptions {
|
|
17
|
+
sessionManager?: SessionManager;
|
|
18
|
+
createProvider?: (providerId: string, apiKey: string, baseURL: string) => Provider;
|
|
19
|
+
registry?: ProviderRegistry;
|
|
20
|
+
skillRegistry?: SkillRegistry;
|
|
21
|
+
planHandlerRef?: PlanHandlerRef;
|
|
22
|
+
approvalHandlerRef?: ApprovalHandlerRef;
|
|
23
|
+
questionController?: QuestionController;
|
|
24
|
+
bashAllowlist?: BashAllowlist;
|
|
25
|
+
settingsManager?: SettingsManager;
|
|
26
|
+
lspService?: LspService;
|
|
27
|
+
mcpManager?: McpManager;
|
|
28
|
+
themeMode?: ThemeMode;
|
|
29
|
+
themeOverrides?: Record<string, string>;
|
|
30
|
+
detectedTheme?: ResolvedTheme;
|
|
31
|
+
onThemeModeChange?: (mode: ThemeMode) => void;
|
|
32
|
+
flushMemory?: () => Promise<void>;
|
|
33
|
+
runMemoryCompaction?: () => Promise<string>;
|
|
34
|
+
runMemorySummary?: (scope?: MemoryScope) => Promise<string>;
|
|
35
|
+
runMemoryRefresh?: (scope?: MemoryScope) => Promise<string>;
|
|
36
|
+
bypassEnabled?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export declare function runTui(agent: Agent, args: CliArgs, options?: RunTuiOptions): Promise<void>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { createCliRenderer } from "@opentui/core";
|
|
3
|
+
import { createRoot } from "@opentui/react";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { App } from "./app.js";
|
|
6
|
+
export async function runTui(agent, args, options = {}) {
|
|
7
|
+
let exitSummary;
|
|
8
|
+
const renderer = await createCliRenderer();
|
|
9
|
+
const root = createRoot(renderer);
|
|
10
|
+
root.render(_jsx(App, { agent: agent, args: args, sessionManager: options.sessionManager, createProvider: options.createProvider, registry: options.registry, skillRegistry: options.skillRegistry, planHandlerRef: options.planHandlerRef, approvalHandlerRef: options.approvalHandlerRef, questionController: options.questionController, bashAllowlist: options.bashAllowlist, settingsManager: options.settingsManager, lspService: options.lspService, mcpManager: options.mcpManager, themeMode: options.themeMode, themeOverrides: options.themeOverrides, detectedTheme: options.detectedTheme, onThemeModeChange: options.onThemeModeChange, flushMemory: options.flushMemory, runMemoryCompaction: options.runMemoryCompaction, runMemorySummary: options.runMemorySummary, runMemoryRefresh: options.runMemoryRefresh, bypassEnabled: options.bypassEnabled, onExit: (summary) => {
|
|
11
|
+
exitSummary = summary;
|
|
12
|
+
} }));
|
|
13
|
+
await new Promise((resolve) => {
|
|
14
|
+
// OpenTUI emits "destroyed" when renderer.destroy() lands. Listen once.
|
|
15
|
+
const onDone = () => {
|
|
16
|
+
try {
|
|
17
|
+
root.unmount();
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// ignore — root may already be torn down
|
|
21
|
+
}
|
|
22
|
+
resolve();
|
|
23
|
+
};
|
|
24
|
+
renderer.on?.("destroyed", onDone) ?? renderer.once?.("destroyed", onDone);
|
|
25
|
+
});
|
|
26
|
+
if (process.stdout.isTTY) {
|
|
27
|
+
process.stdout.write("\n");
|
|
28
|
+
if (exitSummary) {
|
|
29
|
+
process.stdout.write(formatExitSummary(exitSummary) + "\n");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function formatExitSummary(summary) {
|
|
34
|
+
const label = "Total duration:";
|
|
35
|
+
return chalk.dim(`${label} ${formatWallMs(summary.wallMs)}`);
|
|
36
|
+
}
|
|
37
|
+
function formatWallMs(ms) {
|
|
38
|
+
const totalSeconds = Math.max(0, Math.round(ms / 1000));
|
|
39
|
+
if (totalSeconds < 60)
|
|
40
|
+
return `${totalSeconds}s`;
|
|
41
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
42
|
+
const seconds = totalSeconds % 60;
|
|
43
|
+
if (minutes < 60)
|
|
44
|
+
return `${minutes}m ${seconds}s`;
|
|
45
|
+
const hours = Math.floor(minutes / 60);
|
|
46
|
+
const minutesRest = minutes % 60;
|
|
47
|
+
return `${hours}h ${minutesRest}m ${seconds}s`;
|
|
48
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/react */
|
|
2
|
+
import React from "react";
|
|
3
|
+
import type { SessionSummary } from "../session.js";
|
|
4
|
+
export type SessionPickerMode = "current" | "all";
|
|
5
|
+
export interface SessionPickerProps {
|
|
6
|
+
currentCwd: string;
|
|
7
|
+
currentSessions: SessionSummary[];
|
|
8
|
+
allSessions: SessionSummary[];
|
|
9
|
+
onSelect: (file: string) => void;
|
|
10
|
+
onCancel: () => void;
|
|
11
|
+
}
|
|
12
|
+
export declare function SessionPicker({ currentCwd, currentSessions, allSessions, onSelect, onCancel }: SessionPickerProps): React.ReactNode;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
/** @jsxImportSource @opentui/react */
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
|
+
import { useKeyboard } from "@opentui/react";
|
|
5
|
+
import { useTheme } from "./theme.js";
|
|
6
|
+
import { formatRelativeTime } from "./recent-activity.js";
|
|
7
|
+
import { padVisual, truncateVisual } from "../text-display.js";
|
|
8
|
+
import { useTerminalSize } from "./use-terminal-size.js";
|
|
9
|
+
export function SessionPicker({ currentCwd, currentSessions, allSessions, onSelect, onCancel }) {
|
|
10
|
+
const theme = useTheme();
|
|
11
|
+
const { columns: termWidth, rows: termHeight } = useTerminalSize();
|
|
12
|
+
const maxVisible = Math.max(6, termHeight - 10);
|
|
13
|
+
const [mode, setMode] = useState("current");
|
|
14
|
+
const [selectedSessionIdx, setSelectedSessionIdx] = useState(0);
|
|
15
|
+
const rows = useMemo(() => buildRows(mode, currentCwd, currentSessions, allSessions), [mode, currentCwd, currentSessions, allSessions]);
|
|
16
|
+
const sessionRowIndices = useMemo(() => rows.map((row, i) => (row.type === "session" ? i : -1)).filter((i) => i >= 0), [rows]);
|
|
17
|
+
const clampedIdx = sessionRowIndices.length === 0
|
|
18
|
+
? 0
|
|
19
|
+
: Math.min(selectedSessionIdx, sessionRowIndices.length - 1);
|
|
20
|
+
const selectedRowIndex = sessionRowIndices[clampedIdx] ?? -1;
|
|
21
|
+
useKeyboard((key) => {
|
|
22
|
+
if (key.eventType === "release")
|
|
23
|
+
return;
|
|
24
|
+
if (key.name === "escape") {
|
|
25
|
+
onCancel();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (key.name === "tab") {
|
|
29
|
+
setMode((m) => (m === "current" ? "all" : "current"));
|
|
30
|
+
setSelectedSessionIdx(0);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (key.name === "return") {
|
|
34
|
+
const row = rows[selectedRowIndex];
|
|
35
|
+
if (row?.type === "session" && row.session)
|
|
36
|
+
onSelect(row.session.file);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (key.name === "up") {
|
|
40
|
+
setSelectedSessionIdx((i) => Math.max(0, i - 1));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (key.name === "down") {
|
|
44
|
+
setSelectedSessionIdx((i) => Math.min(Math.max(0, sessionRowIndices.length - 1), i + 1));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
// Window the visible rows around the selected session.
|
|
49
|
+
const start = clampWindowStart(rows, selectedRowIndex, maxVisible);
|
|
50
|
+
const visible = rows.slice(start, start + maxVisible);
|
|
51
|
+
const modeLabel = mode === "current" ? "Current dir" : "All directories";
|
|
52
|
+
const totalSessions = sessionRowIndices.length;
|
|
53
|
+
return (_jsxs("box", { style: {
|
|
54
|
+
flexDirection: "column",
|
|
55
|
+
marginTop: 1,
|
|
56
|
+
marginBottom: 1,
|
|
57
|
+
paddingLeft: 1,
|
|
58
|
+
paddingRight: 1,
|
|
59
|
+
border: true,
|
|
60
|
+
borderColor: theme.borderActive,
|
|
61
|
+
}, children: [_jsx("text", { fg: theme.accent, attributes: 1, children: "Resume session" }), _jsxs("box", { style: { flexDirection: "row" }, children: [_jsx("text", { fg: theme.muted, children: "View: " }), _jsx("text", { fg: theme.accent, children: modeLabel }), _jsxs("text", { fg: theme.muted, children: [" · ", totalSessions, " session", totalSessions === 1 ? "" : "s"] })] }), _jsx("text", { fg: theme.muted, children: "\u2191/\u2193 navigate \u00B7 Enter resume \u00B7 Tab toggle scope \u00B7 Esc start fresh" }), _jsxs("box", { style: { flexDirection: "column", marginTop: 1 }, children: [totalSessions === 0 && (_jsx("text", { fg: theme.muted, children: mode === "current"
|
|
62
|
+
? "No previous sessions in this directory."
|
|
63
|
+
: "No previous sessions found." })), visible.map((row, i) => {
|
|
64
|
+
const actualIndex = start + i;
|
|
65
|
+
if (row.type === "header") {
|
|
66
|
+
return (_jsx("box", { style: { marginTop: i === 0 ? 0 : 1 }, children: _jsx("text", { fg: theme.muted, attributes: 1, children: row.label }) }, `h-${actualIndex}`));
|
|
67
|
+
}
|
|
68
|
+
const session = row.session;
|
|
69
|
+
const isSelected = actualIndex === selectedRowIndex;
|
|
70
|
+
const time = padVisual(formatRelativeTime(session.mtime), 9);
|
|
71
|
+
const titleWidth = Math.max(20, Math.min(80, termWidth - 30));
|
|
72
|
+
return (_jsxs("box", { style: { flexDirection: "row" }, children: [_jsxs("text", { fg: isSelected ? theme.accent : undefined, children: [isSelected ? "> " : " ", time, " ", padVisual(truncateVisual(session.title, titleWidth), titleWidth)] }), _jsx("box", { style: { marginLeft: 1 }, children: _jsxs("text", { fg: theme.muted, children: ["\u00B7 ", session.messageCount, " msg", session.messageCount === 1 ? "" : "s"] }) })] }, session.file));
|
|
73
|
+
})] })] }));
|
|
74
|
+
}
|
|
75
|
+
function buildRows(mode, currentCwd, currentSessions, allSessions) {
|
|
76
|
+
if (mode === "current") {
|
|
77
|
+
if (currentSessions.length === 0)
|
|
78
|
+
return [];
|
|
79
|
+
return [
|
|
80
|
+
{ type: "header", label: currentCwd },
|
|
81
|
+
...currentSessions.map((session) => ({ type: "session", session })),
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
const grouped = new Map();
|
|
85
|
+
for (const session of allSessions) {
|
|
86
|
+
const key = session.cwdLabel;
|
|
87
|
+
const list = grouped.get(key);
|
|
88
|
+
if (list)
|
|
89
|
+
list.push(session);
|
|
90
|
+
else
|
|
91
|
+
grouped.set(key, [session]);
|
|
92
|
+
}
|
|
93
|
+
const sortedGroups = Array.from(grouped.entries()).sort((a, b) => {
|
|
94
|
+
if (a[0] === currentCwd)
|
|
95
|
+
return -1;
|
|
96
|
+
if (b[0] === currentCwd)
|
|
97
|
+
return 1;
|
|
98
|
+
const aLatest = a[1][0]?.mtime ?? 0;
|
|
99
|
+
const bLatest = b[1][0]?.mtime ?? 0;
|
|
100
|
+
return bLatest - aLatest;
|
|
101
|
+
});
|
|
102
|
+
const rows = [];
|
|
103
|
+
for (const [label, sessions] of sortedGroups) {
|
|
104
|
+
rows.push({ type: "header", label });
|
|
105
|
+
for (const session of sessions)
|
|
106
|
+
rows.push({ type: "session", session });
|
|
107
|
+
}
|
|
108
|
+
return rows;
|
|
109
|
+
}
|
|
110
|
+
function clampWindowStart(rows, selectedRowIndex, maxVisible) {
|
|
111
|
+
if (rows.length <= maxVisible)
|
|
112
|
+
return 0;
|
|
113
|
+
if (selectedRowIndex < 0)
|
|
114
|
+
return 0;
|
|
115
|
+
const half = Math.floor(maxVisible / 2);
|
|
116
|
+
let start = Math.max(0, selectedRowIndex - half);
|
|
117
|
+
if (start + maxVisible > rows.length)
|
|
118
|
+
start = rows.length - maxVisible;
|
|
119
|
+
return Math.max(0, start);
|
|
120
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme for the OpenTUI TUI, structured after opencode's grayscale + semantic
|
|
3
|
+
* accent model. Bubble keeps the quiet terminal surface, blue focus color, and
|
|
4
|
+
* warm command accent instead of turning light mode into a plain gray port.
|
|
5
|
+
*
|
|
6
|
+
* The exported `Theme` shape preserves the keys consumed by the rest of the
|
|
7
|
+
* TUI so no caller needs to change.
|
|
8
|
+
*/
|
|
9
|
+
export type ResolvedTheme = "light" | "dark";
|
|
10
|
+
export type ThemeMode = "auto" | ResolvedTheme;
|
|
11
|
+
export interface Theme {
|
|
12
|
+
user: string;
|
|
13
|
+
agent: string;
|
|
14
|
+
error: string;
|
|
15
|
+
warning: string;
|
|
16
|
+
success: string;
|
|
17
|
+
accent: string;
|
|
18
|
+
border: string;
|
|
19
|
+
borderActive: string;
|
|
20
|
+
inputBorder: string;
|
|
21
|
+
inputBorderDisabled: string;
|
|
22
|
+
inputBg: string;
|
|
23
|
+
inputBgDisabled: string;
|
|
24
|
+
inputText: string;
|
|
25
|
+
inputPlaceholder: string;
|
|
26
|
+
muted: string;
|
|
27
|
+
dim: string;
|
|
28
|
+
thinking: string;
|
|
29
|
+
thinkingDim: string;
|
|
30
|
+
toolName: string;
|
|
31
|
+
toolResult: string;
|
|
32
|
+
toolError: string;
|
|
33
|
+
toolPending: string;
|
|
34
|
+
code: string;
|
|
35
|
+
traceAction: string;
|
|
36
|
+
traceCount: string;
|
|
37
|
+
traceDetail: string;
|
|
38
|
+
traceCommand: string;
|
|
39
|
+
tracePending: string;
|
|
40
|
+
userMessageBorder: string;
|
|
41
|
+
userMessageBg: string;
|
|
42
|
+
userMessageText: string;
|
|
43
|
+
userRail: string;
|
|
44
|
+
diffAdd: string;
|
|
45
|
+
diffRemove: string;
|
|
46
|
+
diffAddFg: string;
|
|
47
|
+
diffRemoveFg: string;
|
|
48
|
+
toolFile: string;
|
|
49
|
+
toolShell: string;
|
|
50
|
+
toolSearch: string;
|
|
51
|
+
toolThink: string;
|
|
52
|
+
toolNet: string;
|
|
53
|
+
toolEdit: string;
|
|
54
|
+
brand: string;
|
|
55
|
+
brandSoft: string;
|
|
56
|
+
brandDeep: string;
|
|
57
|
+
background: string;
|
|
58
|
+
backgroundPanel: string;
|
|
59
|
+
backgroundElement: string;
|
|
60
|
+
text: string;
|
|
61
|
+
textMuted: string;
|
|
62
|
+
textDim: string;
|
|
63
|
+
surface: string;
|
|
64
|
+
shade: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 12-step grayscale + lavender accent.
|
|
68
|
+
*
|
|
69
|
+
* step1 #0a0a0a root bg
|
|
70
|
+
* step2 #141414 panel bg (dialogs, chips)
|
|
71
|
+
* step3 #1c1820 element bg (input fill, slightly purple-tinged)
|
|
72
|
+
* step4 #232028 surface (current input bg base)
|
|
73
|
+
* step5 #2a2630 raised panel
|
|
74
|
+
* step6 #3a3242 borderSubtle
|
|
75
|
+
* step7 #4a4254 border
|
|
76
|
+
* step8 #5e5570 borderActive
|
|
77
|
+
* step9 #bd91db accent / primary ← brand
|
|
78
|
+
* step10 #d4afe8 accent hover / soft
|
|
79
|
+
* step11 #808080 text muted
|
|
80
|
+
* step12 #eeeeee text
|
|
81
|
+
*/
|
|
82
|
+
export declare const darkTheme: Theme;
|
|
83
|
+
export declare const lightTheme: Theme;
|
|
84
|
+
export declare const ThemeProvider: import("react").Provider<Theme>;
|
|
85
|
+
export declare function useTheme(): Theme;
|
|
86
|
+
export declare function paletteFor(mode: ResolvedTheme, overrides?: Record<string, string>): Theme;
|
|
87
|
+
/** opencode style: tool category is encoded by header text, not color. Keep
|
|
88
|
+
* a single accent for tool names so users get visual consistency. */
|
|
89
|
+
export declare function toolAccent(theme: Theme, _toolName: string): string;
|