@bubblebrain-ai/bubble 0.0.12 → 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 +38 -0
- package/dist/main.js +58 -9
- package/dist/slash-commands/commands.js +27 -0
- package/dist/slash-commands/types.d.ts +10 -0
- 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 +44 -5
- package/dist/tui-ink/message-list.js +9 -1
- 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,52 @@
|
|
|
1
|
+
import { countUnifiedDiffChanges } from "../diff-stats.js";
|
|
2
|
+
export const EDIT_COLLAPSED_DIFF_LINES = 20;
|
|
3
|
+
export function getEditDiffDetails(tool) {
|
|
4
|
+
if (tool.name !== "edit" || tool.isError)
|
|
5
|
+
return null;
|
|
6
|
+
const metadata = tool.metadata;
|
|
7
|
+
const metadataDiff = readMetadataString(metadata, "diff");
|
|
8
|
+
const diff = metadataDiff ?? extractDiffFromResult(tool.result);
|
|
9
|
+
if (!diff)
|
|
10
|
+
return null;
|
|
11
|
+
const counted = countUnifiedDiffChanges(diff);
|
|
12
|
+
const added = readMetadataNumber(metadata, "addedLines") ?? counted.added;
|
|
13
|
+
const removed = readMetadataNumber(metadata, "removedLines") ?? counted.removed;
|
|
14
|
+
const path = readMetadataString(metadata, "path")
|
|
15
|
+
?? (typeof tool.args.path === "string" ? tool.args.path : undefined);
|
|
16
|
+
return { diff, added, removed, path };
|
|
17
|
+
}
|
|
18
|
+
export function formatEditSuccessSummary(details) {
|
|
19
|
+
const stats = details ? formatEditStats(details.added, details.removed) : "";
|
|
20
|
+
return `Succeeded. File edited.${stats ? ` ${stats}` : ""}`;
|
|
21
|
+
}
|
|
22
|
+
export function formatEditStats(added, removed) {
|
|
23
|
+
const parts = [];
|
|
24
|
+
if (added > 0)
|
|
25
|
+
parts.push(`+${added} added`);
|
|
26
|
+
if (removed > 0)
|
|
27
|
+
parts.push(`-${removed} removed`);
|
|
28
|
+
if (parts.length === 0)
|
|
29
|
+
return "";
|
|
30
|
+
return `(${parts.join(", ")})`;
|
|
31
|
+
}
|
|
32
|
+
function extractDiffFromResult(result) {
|
|
33
|
+
if (!result)
|
|
34
|
+
return null;
|
|
35
|
+
const normalized = result.replace(/\r\n/g, "\n");
|
|
36
|
+
const marker = "\nDiff:\n";
|
|
37
|
+
const index = normalized.indexOf(marker);
|
|
38
|
+
if (index === -1)
|
|
39
|
+
return null;
|
|
40
|
+
const rawDiff = normalized.slice(index + marker.length);
|
|
41
|
+
const diagnosticsIndex = rawDiff.search(/\n\nLSP diagnostics in /);
|
|
42
|
+
const diff = diagnosticsIndex === -1 ? rawDiff : rawDiff.slice(0, diagnosticsIndex);
|
|
43
|
+
return diff.trim().length > 0 ? diff : null;
|
|
44
|
+
}
|
|
45
|
+
function readMetadataString(metadata, key) {
|
|
46
|
+
const value = metadata?.[key];
|
|
47
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
48
|
+
}
|
|
49
|
+
function readMetadataNumber(metadata, key) {
|
|
50
|
+
const value = metadata?.[key];
|
|
51
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
52
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/react */
|
|
2
|
+
import React from "react";
|
|
3
|
+
import type { FeedbackPayload } from "../feedback/types.js";
|
|
4
|
+
interface FeedbackDialogProps {
|
|
5
|
+
/** Pre-collected env + transcript; description is filled in by the user. */
|
|
6
|
+
base: Omit<FeedbackPayload, "description">;
|
|
7
|
+
initialDescription: string;
|
|
8
|
+
onDismiss: () => void;
|
|
9
|
+
onResult: (result: {
|
|
10
|
+
kind: "success";
|
|
11
|
+
url: string;
|
|
12
|
+
number: number;
|
|
13
|
+
} | {
|
|
14
|
+
kind: "error";
|
|
15
|
+
message: string;
|
|
16
|
+
} | {
|
|
17
|
+
kind: "cancelled";
|
|
18
|
+
}) => void;
|
|
19
|
+
}
|
|
20
|
+
export declare function FeedbackDialog({ base, initialDescription, onDismiss, onResult }: FeedbackDialogProps): React.ReactNode;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } 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 { submitFeedback, FeedbackSubmitError } from "../feedback/submit.js";
|
|
7
|
+
export function FeedbackDialog({ base, initialDescription, onDismiss, onResult }) {
|
|
8
|
+
const theme = useTheme();
|
|
9
|
+
const [stage, setStage] = useState("edit");
|
|
10
|
+
const [description, setDescription] = useState(initialDescription);
|
|
11
|
+
const [cursor, setCursor] = useState(initialDescription.length);
|
|
12
|
+
const [showPreview, setShowPreview] = useState(false);
|
|
13
|
+
const [finalResult, setFinalResult] = useState(null);
|
|
14
|
+
const transcriptStats = useMemo(() => {
|
|
15
|
+
const total = base.transcript.reduce((sum, m) => sum + m.content.length, 0);
|
|
16
|
+
return { count: base.transcript.length, totalChars: total };
|
|
17
|
+
}, [base.transcript]);
|
|
18
|
+
const insertAtCursor = (text) => {
|
|
19
|
+
setDescription((prev) => prev.slice(0, cursor) + text + prev.slice(cursor));
|
|
20
|
+
setCursor((c) => c + text.length);
|
|
21
|
+
};
|
|
22
|
+
const submit = async () => {
|
|
23
|
+
setStage("submitting");
|
|
24
|
+
const payload = { ...base, description: description.trim() };
|
|
25
|
+
try {
|
|
26
|
+
const result = await submitFeedback(payload);
|
|
27
|
+
setFinalResult({ kind: "success", result });
|
|
28
|
+
setStage("done");
|
|
29
|
+
onResult({ kind: "success", url: result.url, number: result.number });
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
const message = err instanceof FeedbackSubmitError
|
|
33
|
+
? err.message
|
|
34
|
+
: err instanceof Error
|
|
35
|
+
? err.message
|
|
36
|
+
: String(err);
|
|
37
|
+
setFinalResult({ kind: "error", message });
|
|
38
|
+
setStage("done");
|
|
39
|
+
onResult({ kind: "error", message });
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
useKeyboard((key) => {
|
|
43
|
+
if (key.eventType === "release")
|
|
44
|
+
return;
|
|
45
|
+
if (stage === "submitting")
|
|
46
|
+
return;
|
|
47
|
+
if (stage === "done") {
|
|
48
|
+
if (key.name === "return" || key.name === "escape" || key.name === " " || key.name === "space") {
|
|
49
|
+
onDismiss();
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// edit stage
|
|
54
|
+
if (key.name === "escape") {
|
|
55
|
+
onResult({ kind: "cancelled" });
|
|
56
|
+
onDismiss();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (key.name === "tab") {
|
|
60
|
+
setShowPreview((v) => !v);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (key.ctrl && (key.name === "d" || key.name === "s")) {
|
|
64
|
+
if (description.trim().length === 0 && transcriptStats.count === 0) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
void submit();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (key.name === "return") {
|
|
71
|
+
insertAtCursor("\n");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (key.name === "backspace" || key.name === "delete") {
|
|
75
|
+
if (cursor > 0) {
|
|
76
|
+
setDescription((prev) => prev.slice(0, cursor - 1) + prev.slice(cursor));
|
|
77
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (key.name === "left") {
|
|
82
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (key.name === "right") {
|
|
86
|
+
setCursor((c) => Math.min(description.length, c + 1));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (key.name === "up" || key.name === "down") {
|
|
90
|
+
const before = description.slice(0, cursor);
|
|
91
|
+
const after = description.slice(cursor);
|
|
92
|
+
const beforeLines = before.split("\n");
|
|
93
|
+
const afterLines = after.split("\n");
|
|
94
|
+
const currentCol = beforeLines[beforeLines.length - 1].length;
|
|
95
|
+
if (key.name === "up" && beforeLines.length > 1) {
|
|
96
|
+
const prevLine = beforeLines[beforeLines.length - 2];
|
|
97
|
+
const col = Math.min(currentCol, prevLine.length);
|
|
98
|
+
const newCursor = before.length - beforeLines[beforeLines.length - 1].length - 1 - (prevLine.length - col);
|
|
99
|
+
setCursor(Math.max(0, newCursor));
|
|
100
|
+
}
|
|
101
|
+
else if (key.name === "down" && afterLines.length > 1) {
|
|
102
|
+
const nextLine = afterLines[1];
|
|
103
|
+
const col = Math.min(currentCol, nextLine.length);
|
|
104
|
+
const newCursor = before.length + afterLines[0].length + 1 + col;
|
|
105
|
+
setCursor(Math.min(description.length, newCursor));
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (key.name && key.name.length === 1 && !key.option) {
|
|
110
|
+
insertAtCursor(key.name);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
if (stage === "done" && finalResult) {
|
|
114
|
+
return (_jsxs("box", { style: {
|
|
115
|
+
flexDirection: "column",
|
|
116
|
+
border: true,
|
|
117
|
+
borderColor: theme.accent,
|
|
118
|
+
paddingLeft: 1,
|
|
119
|
+
paddingRight: 1,
|
|
120
|
+
marginTop: 1,
|
|
121
|
+
marginBottom: 1,
|
|
122
|
+
}, children: [finalResult.kind === "success" ? (_jsxs(_Fragment, { children: [_jsx("text", { fg: theme.accent, attributes: 1, children: "Feedback submitted" }), _jsx("box", { style: { marginTop: 1 }, children: _jsxs("text", { children: ["Thanks! Issue #", finalResult.result.number, " created."] }) }), _jsx("box", { style: { marginTop: 1 }, children: _jsx("text", { fg: theme.muted, children: finalResult.result.url }) })] })) : (_jsxs(_Fragment, { children: [_jsx("text", { fg: "red", attributes: 1, children: "Feedback failed to submit" }), _jsx("box", { style: { marginTop: 1 }, children: _jsx("text", { children: finalResult.message }) })] })), _jsx("box", { style: { marginTop: 1 }, children: _jsx("text", { fg: theme.muted, children: "Press Enter to dismiss" }) })] }));
|
|
123
|
+
}
|
|
124
|
+
if (stage === "submitting") {
|
|
125
|
+
return (_jsx("box", { style: {
|
|
126
|
+
flexDirection: "column",
|
|
127
|
+
border: true,
|
|
128
|
+
borderColor: theme.accent,
|
|
129
|
+
paddingLeft: 1,
|
|
130
|
+
paddingRight: 1,
|
|
131
|
+
marginTop: 1,
|
|
132
|
+
marginBottom: 1,
|
|
133
|
+
}, children: _jsx("text", { fg: theme.accent, attributes: 1, children: "Sending feedback..." }) }));
|
|
134
|
+
}
|
|
135
|
+
// edit stage
|
|
136
|
+
return (_jsxs("box", { style: {
|
|
137
|
+
flexDirection: "column",
|
|
138
|
+
border: true,
|
|
139
|
+
borderColor: theme.accent,
|
|
140
|
+
paddingLeft: 1,
|
|
141
|
+
paddingRight: 1,
|
|
142
|
+
marginTop: 1,
|
|
143
|
+
marginBottom: 1,
|
|
144
|
+
}, children: [_jsx("text", { fg: theme.accent, attributes: 1, children: "Send feedback" }), _jsx("box", { style: { marginTop: 1 }, children: _jsx("text", { fg: "yellow", children: "This creates a PUBLIC GitHub issue at DylanDDeng/bubble. Review before sending." }) }), _jsxs("box", { style: { marginTop: 1, flexDirection: "column" }, children: [_jsx("text", { fg: theme.muted, children: "Describe what happened:" }), _jsx("box", { style: {
|
|
145
|
+
border: true,
|
|
146
|
+
borderColor: theme.muted,
|
|
147
|
+
paddingLeft: 1,
|
|
148
|
+
paddingRight: 1,
|
|
149
|
+
marginTop: 0,
|
|
150
|
+
minHeight: 3,
|
|
151
|
+
}, children: _jsx("text", { children: renderWithCursor(description, cursor) }) })] }), _jsxs("box", { style: { marginTop: 1, flexDirection: "column" }, children: [_jsxs("text", { fg: theme.muted, children: ["Also included: v", base.version, " \u00B7 ", base.platform, "/", base.arch, " \u00B7 node ", base.nodeVersion, " \u00B7", " ", base.provider, "/", base.model, " \u00B7 ", transcriptStats.count, " messages (", transcriptStats.totalChars, " chars, secrets redacted)"] }), showPreview && (_jsxs("box", { style: {
|
|
152
|
+
flexDirection: "column",
|
|
153
|
+
marginTop: 1,
|
|
154
|
+
border: true,
|
|
155
|
+
borderColor: theme.muted,
|
|
156
|
+
paddingLeft: 1,
|
|
157
|
+
paddingRight: 1,
|
|
158
|
+
}, children: [_jsx("text", { fg: theme.muted, attributes: 1, children: "Payload preview (exactly what will be submitted):" }), base.transcript.map((m, i) => (_jsxs("box", { style: { flexDirection: "column", marginTop: 1 }, children: [_jsxs("text", { fg: theme.accent, children: ["[", m.role, "]"] }), _jsx("text", { children: m.content })] }, i))), base.recentError && (_jsxs("box", { style: { flexDirection: "column", marginTop: 1 }, children: [_jsx("text", { fg: "red", children: "[recent error]" }), _jsx("text", { children: base.recentError })] }))] }))] }), _jsx("box", { style: { marginTop: 1, flexDirection: "row" }, children: _jsxs("text", { fg: theme.muted, children: [_jsx("text", { fg: theme.accent, attributes: 1, children: "Ctrl+D" }), " submit · ", _jsx("text", { fg: theme.accent, attributes: 1, children: "Tab" }), " ", showPreview ? "hide" : "view", " payload · ", _jsx("text", { fg: theme.accent, attributes: 1, children: "Enter" }), " newline · ", _jsx("text", { fg: theme.accent, attributes: 1, children: "Esc" }), " cancel"] }) })] }));
|
|
159
|
+
}
|
|
160
|
+
function renderWithCursor(text, cursor) {
|
|
161
|
+
if (text.length === 0)
|
|
162
|
+
return "▏";
|
|
163
|
+
return text.slice(0, cursor) + "▏" + text.slice(cursor);
|
|
164
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/react */
|
|
2
|
+
import React from "react";
|
|
3
|
+
export interface FeishuSetupPickerProps {
|
|
4
|
+
onComplete: (summary: string) => void;
|
|
5
|
+
onCancel: () => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function FeishuSetupPicker({ onComplete, onCancel }: FeishuSetupPickerProps): React.ReactNode;
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
/** @jsxImportSource @opentui/react */
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { useKeyboard } from "@opentui/react";
|
|
5
|
+
import qrTerminal from "qrcode-terminal";
|
|
6
|
+
import { existsSync, statSync } from "node:fs";
|
|
7
|
+
import { isAbsolute, resolve as resolvePath, basename } from "node:path";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { registerApp } from "@larksuiteoapi/node-sdk";
|
|
10
|
+
import { useTheme } from "./theme.js";
|
|
11
|
+
import { bootstrapConfig } from "../feishu/config.js";
|
|
12
|
+
import { ScopeRegistry } from "../feishu/scope/scope-registry.js";
|
|
13
|
+
const EMPTY_VALUES = { chatId: "", cwd: "", displayName: "" };
|
|
14
|
+
export function FeishuSetupPicker({ onComplete, onCancel }) {
|
|
15
|
+
const theme = useTheme();
|
|
16
|
+
const [stage, setStage] = useState({ kind: "registering" });
|
|
17
|
+
const abortRef = useRef(undefined);
|
|
18
|
+
const completedRef = useRef(false);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const controller = new AbortController();
|
|
21
|
+
abortRef.current = controller;
|
|
22
|
+
let cancelled = false;
|
|
23
|
+
void (async () => {
|
|
24
|
+
try {
|
|
25
|
+
const result = await registerApp({
|
|
26
|
+
signal: controller.signal,
|
|
27
|
+
onQRCodeReady: (info) => {
|
|
28
|
+
if (cancelled)
|
|
29
|
+
return;
|
|
30
|
+
qrTerminal.generate(info.url, { small: true }, (ascii) => {
|
|
31
|
+
if (cancelled)
|
|
32
|
+
return;
|
|
33
|
+
setStage({
|
|
34
|
+
kind: "qr_shown",
|
|
35
|
+
url: info.url,
|
|
36
|
+
ascii,
|
|
37
|
+
status: "等待扫码…",
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
onStatusChange: (info) => {
|
|
42
|
+
if (cancelled)
|
|
43
|
+
return;
|
|
44
|
+
setStage((prev) => {
|
|
45
|
+
if (prev.kind !== "qr_shown")
|
|
46
|
+
return prev;
|
|
47
|
+
const label = info.status === "polling"
|
|
48
|
+
? "等待扫码…"
|
|
49
|
+
: info.status === "slow_down"
|
|
50
|
+
? "轮询变慢中…仍在等待"
|
|
51
|
+
: info.status === "domain_switched"
|
|
52
|
+
? "已切换域名"
|
|
53
|
+
: info.status;
|
|
54
|
+
return { ...prev, status: label };
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
if (cancelled)
|
|
59
|
+
return;
|
|
60
|
+
const ownerOpenId = result.user_info?.open_id;
|
|
61
|
+
if (!ownerOpenId) {
|
|
62
|
+
setStage({ kind: "error", message: "授权成功但没拿到 owner open_id,无法继续。" });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
bootstrapConfig({
|
|
67
|
+
appId: result.client_id,
|
|
68
|
+
appSecret: result.client_secret,
|
|
69
|
+
ownerOpenId,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
setStage({ kind: "error", message: `保存 config 失败:${err.message}` });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
setStage({ kind: "credentialed", ownerOpenId, configWritten: true });
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
if (cancelled || controller.signal.aborted)
|
|
80
|
+
return;
|
|
81
|
+
setStage({ kind: "error", message: err.message || "扫码注册失败" });
|
|
82
|
+
}
|
|
83
|
+
})();
|
|
84
|
+
return () => {
|
|
85
|
+
cancelled = true;
|
|
86
|
+
controller.abort();
|
|
87
|
+
};
|
|
88
|
+
}, []);
|
|
89
|
+
const finish = (summary) => {
|
|
90
|
+
if (completedRef.current)
|
|
91
|
+
return;
|
|
92
|
+
completedRef.current = true;
|
|
93
|
+
setStage({ kind: "done", summary });
|
|
94
|
+
onComplete(summary);
|
|
95
|
+
};
|
|
96
|
+
const cancel = () => {
|
|
97
|
+
if (completedRef.current)
|
|
98
|
+
return;
|
|
99
|
+
completedRef.current = true;
|
|
100
|
+
abortRef.current?.abort();
|
|
101
|
+
onCancel();
|
|
102
|
+
};
|
|
103
|
+
useKeyboard((key) => {
|
|
104
|
+
if (key.eventType === "release")
|
|
105
|
+
return;
|
|
106
|
+
if (key.name === "escape") {
|
|
107
|
+
// Esc at any stage = cancel/skip.
|
|
108
|
+
if (stage.kind === "credentialed") {
|
|
109
|
+
finish(`✅ 应用已注册并保存到 ~/.bubble/feishu/。owner: ${stage.ownerOpenId}\n(已跳过 chat 绑定 — 稍后可以编辑 ~/.bubble/feishu/scopes.json 添加)`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (stage.kind === "binding") {
|
|
113
|
+
finish(`✅ 应用已注册。owner: ${stage.ownerOpenId}\n(已跳过 chat 绑定 — 稍后可以 /feishu setup 重来或编辑 scopes.json)`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
cancel();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (stage.kind === "credentialed" && key.name === "return") {
|
|
120
|
+
setStage({
|
|
121
|
+
kind: "binding",
|
|
122
|
+
ownerOpenId: stage.ownerOpenId,
|
|
123
|
+
field: "chatId",
|
|
124
|
+
values: EMPTY_VALUES,
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (stage.kind === "error" && key.name === "return") {
|
|
129
|
+
onCancel();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (stage.kind !== "binding")
|
|
133
|
+
return;
|
|
134
|
+
const cur = stage;
|
|
135
|
+
const updateValue = (next) => {
|
|
136
|
+
setStage({ ...cur, values: { ...cur.values, [cur.field]: next }, error: undefined });
|
|
137
|
+
};
|
|
138
|
+
if (key.name === "return") {
|
|
139
|
+
const submitField = cur.field;
|
|
140
|
+
const value = cur.values[submitField];
|
|
141
|
+
if (submitField === "chatId") {
|
|
142
|
+
if (!value.trim()) {
|
|
143
|
+
setStage({ ...cur, error: "Chat ID 不能为空(oc_...)" });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
setStage({ ...cur, field: "cwd", error: undefined });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (submitField === "cwd") {
|
|
150
|
+
const expanded = expandUser(value.trim());
|
|
151
|
+
if (!isAbsolute(expanded)) {
|
|
152
|
+
setStage({ ...cur, error: "cwd 必须是绝对路径或 ~/..." });
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (!existsSync(expanded) || !statSync(expanded).isDirectory()) {
|
|
156
|
+
setStage({ ...cur, error: `路径不存在或不是目录:${expanded}` });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// Pre-fill display name with basename if user left it empty later.
|
|
160
|
+
const nextDisplayName = cur.values.displayName || basename(expanded);
|
|
161
|
+
setStage({
|
|
162
|
+
...cur,
|
|
163
|
+
field: "displayName",
|
|
164
|
+
values: { ...cur.values, cwd: expanded, displayName: nextDisplayName },
|
|
165
|
+
error: undefined,
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// displayName
|
|
170
|
+
const displayName = value.trim() || basename(cur.values.cwd);
|
|
171
|
+
try {
|
|
172
|
+
const registry = ScopeRegistry.load();
|
|
173
|
+
const scope = {
|
|
174
|
+
cwd: cur.values.cwd,
|
|
175
|
+
displayName,
|
|
176
|
+
allowedUsers: [cur.ownerOpenId],
|
|
177
|
+
admins: [cur.ownerOpenId],
|
|
178
|
+
defaultPermissionMode: "default",
|
|
179
|
+
model: null,
|
|
180
|
+
createdAt: Date.now(),
|
|
181
|
+
lastActiveAt: Date.now(),
|
|
182
|
+
};
|
|
183
|
+
registry.upsert(cur.values.chatId.trim(), scope);
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
setStage({ ...cur, error: `保存 scope 失败:${err.message}` });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
finish(`✅ 已注册应用并绑定第一个 chat:\n chat: ${cur.values.chatId.trim()}\n cwd: ${cur.values.cwd}\n现在可以 /feishu start 启动服务。`);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (key.name === "backspace" || key.name === "delete") {
|
|
193
|
+
updateValue(cur.values[cur.field].slice(0, -1));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (key.name === "tab" && cur.field === "displayName") {
|
|
197
|
+
// Tab in displayName field = use default (basename).
|
|
198
|
+
updateValue(basename(cur.values.cwd));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (key.name && key.name.length === 1 && !key.ctrl && !key.option) {
|
|
202
|
+
updateValue(cur.values[cur.field] + key.name);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
return (_jsxs("box", { style: {
|
|
206
|
+
flexDirection: "column",
|
|
207
|
+
marginTop: 1,
|
|
208
|
+
marginBottom: 1,
|
|
209
|
+
paddingLeft: 1,
|
|
210
|
+
paddingRight: 1,
|
|
211
|
+
border: true,
|
|
212
|
+
borderColor: theme.borderActive,
|
|
213
|
+
}, children: [_jsx("text", { fg: theme.accent, attributes: 1, children: "Feishu Setup Wizard" }), _jsx("text", { fg: theme.muted, children: renderHint(stage) }), _jsx("box", { style: { marginTop: 1, flexDirection: "column" }, children: renderBody(stage, theme) })] }));
|
|
214
|
+
}
|
|
215
|
+
function renderHint(stage) {
|
|
216
|
+
switch (stage.kind) {
|
|
217
|
+
case "registering": return "Esc 取消";
|
|
218
|
+
case "qr_shown": return "用手机飞书扫码 · Esc 取消";
|
|
219
|
+
case "credentialed": return "Enter 绑定第一个 chat · Esc 跳过(之后可手动配置 scopes.json)";
|
|
220
|
+
case "binding": return "输入后 Enter 下一步 · Esc 跳过绑定";
|
|
221
|
+
case "done": return "Enter 关闭";
|
|
222
|
+
case "error": return "Enter 关闭";
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function renderBody(stage, theme) {
|
|
226
|
+
switch (stage.kind) {
|
|
227
|
+
case "registering":
|
|
228
|
+
return _jsx("text", { fg: theme.muted, children: "\u6B63\u5728\u5411\u98DE\u4E66\u7533\u8BF7\u6CE8\u518C\u7801\u2026" });
|
|
229
|
+
case "qr_shown":
|
|
230
|
+
return (_jsxs("box", { style: { flexDirection: "column" }, children: [_jsx("text", { fg: theme.muted, children: stage.status }), _jsx("box", { style: { marginTop: 1, flexDirection: "column" }, children: stage.ascii.split("\n").map((line, i) => (_jsx("text", { children: line || " " }, `q-${i}`))) }), _jsxs("box", { style: { marginTop: 1, flexDirection: "column" }, children: [_jsx("text", { fg: theme.muted, children: "\u626B\u4E0D\u5230\uFF1F\u4E5F\u53EF\u4EE5\u6D4F\u89C8\u5668\u6253\u5F00\uFF1A" }), _jsx("text", { children: stage.url })] })] }));
|
|
231
|
+
case "credentialed":
|
|
232
|
+
return (_jsxs("box", { style: { flexDirection: "column" }, children: [_jsx("text", { fg: theme.accent, children: "\u2705 \u6CE8\u518C\u6210\u529F" }), _jsxs("box", { style: { flexDirection: "row" }, children: [_jsx("text", { children: "owner open_id: " }), _jsx("text", { fg: theme.accent, children: stage.ownerOpenId })] }), _jsx("box", { style: { marginTop: 1 }, children: _jsx("text", { fg: theme.muted, children: "\u5DF2\u5199\u5165 ~/.bubble/feishu/config.json + secrets.enc\uFF08\u52A0\u5BC6\uFF09\u3002" }) }), _jsx("box", { style: { marginTop: 1 }, children: _jsx("text", { children: "\u4E0B\u4E00\u6B65\uFF1A\u628A\u4E00\u4E2A\u98DE\u4E66 chat \u7ED1\u5B9A\u5230\u672C\u5730\u76EE\u5F55\uFF1F" }) })] }));
|
|
233
|
+
case "binding":
|
|
234
|
+
return _jsx(BindingForm, { stage: stage, theme: theme });
|
|
235
|
+
case "done":
|
|
236
|
+
return (_jsx("box", { style: { flexDirection: "column" }, children: stage.summary.split("\n").map((line, i) => (_jsx("text", { children: line }, i))) }));
|
|
237
|
+
case "error":
|
|
238
|
+
return (_jsxs("box", { style: { flexDirection: "column" }, children: [_jsxs("text", { fg: "red", children: ["\u274C ", stage.message] }), _jsx("box", { style: { marginTop: 1 }, children: _jsx("text", { fg: theme.muted, children: "\u6309 Enter \u5173\u95ED\u3002\u53EF\u4EE5\u7A0D\u540E\u518D /feishu setup \u91CD\u8BD5\u3002" }) })] }));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function BindingForm({ stage, theme }) {
|
|
242
|
+
const labels = {
|
|
243
|
+
chatId: {
|
|
244
|
+
label: "Chat ID",
|
|
245
|
+
hint: "飞书 chat 的 oc_ 开头 ID。⚠️ 现在你大概率还不知道这个 —— 按 Esc 跳过,先 /feishu start 起服务,给 bot 发条消息后用 /feishu discover 自动获取。",
|
|
246
|
+
},
|
|
247
|
+
cwd: {
|
|
248
|
+
label: "本地 cwd",
|
|
249
|
+
hint: `例如 ${homedir()}/projects/my-app(绝对路径或 ~/...)`,
|
|
250
|
+
},
|
|
251
|
+
displayName: {
|
|
252
|
+
label: "显示名(可空,默认 = 目录名)",
|
|
253
|
+
hint: "出现在飞书卡片顶栏的短标签",
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
return (_jsxs("box", { style: { flexDirection: "column" }, children: [Object.keys(labels).map((field) => {
|
|
257
|
+
const meta = labels[field];
|
|
258
|
+
const value = stage.values[field];
|
|
259
|
+
const isActive = stage.field === field;
|
|
260
|
+
const isDone = !isActive && value && fieldOrderIndex(stage.field) > fieldOrderIndex(field);
|
|
261
|
+
const marker = isActive ? "› " : isDone ? "✓ " : " ";
|
|
262
|
+
return (_jsxs("box", { style: { flexDirection: "column", marginBottom: isActive ? 1 : 0 }, children: [_jsxs("box", { style: { flexDirection: "row" }, children: [_jsxs("text", { fg: isActive ? theme.accent : isDone ? "green" : theme.muted, children: [marker, meta.label, ":"] }), _jsx("box", { style: { marginLeft: 1 }, children: _jsxs("text", { children: [value, isActive ? "▌" : ""] }) })] }), isActive && (_jsx("box", { style: { marginLeft: 2 }, children: _jsx("text", { fg: theme.muted, children: meta.hint }) }))] }, field));
|
|
263
|
+
}), stage.error && (_jsx("box", { style: { marginTop: 1 }, children: _jsx("text", { fg: "red", children: stage.error }) }))] }));
|
|
264
|
+
}
|
|
265
|
+
function fieldOrderIndex(field) {
|
|
266
|
+
return field === "chatId" ? 0 : field === "cwd" ? 1 : 2;
|
|
267
|
+
}
|
|
268
|
+
function expandUser(p) {
|
|
269
|
+
if (p === "~" || p.startsWith("~/"))
|
|
270
|
+
return homedir() + p.slice(1);
|
|
271
|
+
return resolvePath(p);
|
|
272
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface AtContext {
|
|
2
|
+
start: number;
|
|
3
|
+
end: number;
|
|
4
|
+
query: string;
|
|
5
|
+
}
|
|
6
|
+
export interface FileSuggestion {
|
|
7
|
+
path: string;
|
|
8
|
+
score: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ExpandedMention {
|
|
11
|
+
path: string;
|
|
12
|
+
bytes: number;
|
|
13
|
+
truncated: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface ExpandResult {
|
|
16
|
+
text: string;
|
|
17
|
+
expanded: ExpandedMention[];
|
|
18
|
+
missing: string[];
|
|
19
|
+
skipped: Array<{
|
|
20
|
+
path: string;
|
|
21
|
+
reason: string;
|
|
22
|
+
bytes?: number;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
export declare function findAtContext(text: string, cursor: number): AtContext | null;
|
|
26
|
+
export declare function filterFileSuggestions(files: string[], query: string, limit?: number): FileSuggestion[];
|
|
27
|
+
export declare function listProjectFiles(cwd: string): Promise<string[]>;
|
|
28
|
+
export declare function invalidateFileListCache(cwd?: string): void;
|
|
29
|
+
export declare function expandAtMentions(text: string, cwd: string): Promise<ExpandResult>;
|