@bubblebrain-ai/bubble 0.0.20 → 0.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/abort-errors.d.ts +14 -0
- package/dist/agent/abort-errors.js +21 -0
- package/dist/agent/budget-ledger.d.ts +41 -0
- package/dist/agent/budget-ledger.js +64 -0
- package/dist/agent/child-runner.d.ts +55 -0
- package/dist/agent/child-runner.js +312 -0
- package/dist/agent/profiles.d.ts +8 -0
- package/dist/agent/profiles.js +27 -5
- package/dist/agent/result-integrator.d.ts +22 -0
- package/dist/agent/result-integrator.js +50 -0
- package/dist/agent/subagent-control.d.ts +31 -0
- package/dist/agent/subagent-control.js +27 -0
- package/dist/agent/subagent-lifecycle-reminder.js +11 -2
- package/dist/agent/subagent-scheduler.d.ts +95 -0
- package/dist/agent/subagent-scheduler.js +256 -0
- package/dist/agent/subagent-store.d.ts +41 -0
- package/dist/agent/subagent-store.js +149 -0
- package/dist/agent/subagent-summary.d.ts +30 -0
- package/dist/agent/subagent-summary.js +74 -0
- package/dist/agent/worktree.d.ts +29 -0
- package/dist/agent/worktree.js +73 -0
- package/dist/agent.d.ts +64 -5
- package/dist/agent.js +365 -288
- package/dist/approval/controller.js +9 -1
- package/dist/approval/tool-helper.js +2 -0
- package/dist/approval/types.d.ts +17 -1
- package/dist/checkpoints.d.ts +57 -0
- package/dist/checkpoints.js +0 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +17 -0
- package/dist/feishu/agent-host/approval-card.js +9 -0
- package/dist/feishu/agent-host/run-driver.js +2 -0
- package/dist/main.js +88 -13
- package/dist/network/errors.d.ts +28 -0
- package/dist/network/errors.js +24 -0
- package/dist/orchestrator/default-hooks.js +5 -1
- package/dist/prompt/compose.js +3 -0
- package/dist/prompt/delegation.d.ts +14 -0
- package/dist/prompt/delegation.js +64 -0
- package/dist/prompt/task-reminders.d.ts +5 -1
- package/dist/prompt/task-reminders.js +10 -2
- package/dist/provider-anthropic.js +23 -0
- package/dist/provider.js +23 -3
- package/dist/session.d.ts +31 -0
- package/dist/session.js +69 -0
- package/dist/slash-commands/commands.js +109 -2
- package/dist/slash-commands/types.d.ts +6 -0
- package/dist/tools/agent-lifecycle.d.ts +29 -3
- package/dist/tools/agent-lifecycle.js +394 -40
- package/dist/tools/bash.js +4 -0
- package/dist/tools/child-tools.d.ts +31 -0
- package/dist/tools/child-tools.js +106 -0
- package/dist/tools/edit.d.ts +2 -1
- package/dist/tools/edit.js +2 -1
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.js +3 -3
- package/dist/tools/write.d.ts +2 -1
- package/dist/tools/write.js +2 -1
- package/dist/tui/image-paste.d.ts +18 -0
- package/dist/tui/image-paste.js +60 -0
- package/dist/tui/run.d.ts +11 -1
- package/dist/tui/run.js +399 -71
- package/dist/tui/session-picker-data.d.ts +18 -0
- package/dist/tui/session-picker-data.js +21 -0
- package/dist/tui/trace-groups.d.ts +16 -0
- package/dist/tui/trace-groups.js +42 -1
- package/dist/tui/transcript-scroll.d.ts +25 -0
- package/dist/tui/transcript-scroll.js +20 -0
- package/dist/tui/wordmark.d.ts +2 -0
- package/dist/tui/wordmark.js +31 -4
- package/dist/tui-ink/app.d.ts +4 -1
- package/dist/tui-ink/app.js +301 -247
- package/dist/tui-ink/approval/approval-dialog.js +10 -0
- package/dist/tui-ink/display-history.d.ts +16 -1
- package/dist/tui-ink/display-history.js +50 -21
- package/dist/tui-ink/footer.d.ts +6 -12
- package/dist/tui-ink/footer.js +10 -29
- package/dist/tui-ink/image-paste.d.ts +59 -0
- package/dist/tui-ink/image-paste.js +277 -0
- package/dist/tui-ink/input-box.d.ts +26 -1
- package/dist/tui-ink/input-box.js +171 -41
- package/dist/tui-ink/message-list.d.ts +1 -1
- package/dist/tui-ink/message-list.js +46 -29
- package/dist/tui-ink/run.d.ts +7 -2
- package/dist/tui-ink/run.js +73 -23
- package/dist/tui-ink/terminal-mouse.d.ts +1 -0
- package/dist/tui-ink/terminal-mouse.js +4 -0
- package/dist/tui-ink/trace-groups.d.ts +16 -0
- package/dist/tui-ink/trace-groups.js +50 -2
- package/dist/tui-ink/transcript-viewport-math.d.ts +11 -0
- package/dist/tui-ink/transcript-viewport-math.js +17 -0
- package/dist/tui-ink/transcript-viewport.d.ts +24 -0
- package/dist/tui-ink/transcript-viewport.js +83 -0
- package/dist/tui-ink/welcome.d.ts +9 -7
- package/dist/tui-ink/welcome.js +7 -33
- package/dist/tui-opentui/approval/approval-dialog.js +10 -0
- package/dist/types.d.ts +17 -0
- package/package.json +1 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-child tool factory for write_worktree subagents (design doc §8).
|
|
3
|
+
*
|
|
4
|
+
* Parent tools close over the parent cwd at creation, so a write child needs
|
|
5
|
+
* fresh instances bound to its worktree — with their own FileStateTracker —
|
|
6
|
+
* plus a worktree-scoped approval policy: file operations are runtime-checked
|
|
7
|
+
* to stay under the worktree root (the tools' own workspace fence does this
|
|
8
|
+
* structurally), bash auto-approves inside the worktree when the command
|
|
9
|
+
* passes a deny-list of escaping operations, and everything else fails fast.
|
|
10
|
+
*/
|
|
11
|
+
import { isAbsolute, resolve, sep } from "node:path";
|
|
12
|
+
import { createBashTool } from "./bash.js";
|
|
13
|
+
import { createEditTool } from "./edit.js";
|
|
14
|
+
import { createGlobTool } from "./glob.js";
|
|
15
|
+
import { createGrepTool } from "./grep.js";
|
|
16
|
+
import { createReadTool } from "./read.js";
|
|
17
|
+
import { createWriteTool } from "./write.js";
|
|
18
|
+
import { FileStateTracker } from "./file-state.js";
|
|
19
|
+
/** Operations a worktree child may never run, regardless of cwd. */
|
|
20
|
+
const WORKTREE_BASH_DENY_PATTERNS = [
|
|
21
|
+
{ pattern: /\bgit\s+push\b/, reason: "pushing from a subagent worktree is not allowed; the parent reviews and applies changes." },
|
|
22
|
+
{ pattern: /\bgit\s+remote\b/, reason: "remote configuration is not allowed inside a subagent worktree." },
|
|
23
|
+
{ pattern: /\bgit\s+worktree\b/, reason: "managing worktrees from inside a subagent worktree is not allowed." },
|
|
24
|
+
{ pattern: /\bsudo\b/, reason: "privileged commands are not allowed inside a subagent worktree." },
|
|
25
|
+
];
|
|
26
|
+
export function isPathInsideWorktree(worktreeRoot, candidate) {
|
|
27
|
+
const resolved = isAbsolute(candidate) ? resolve(candidate) : resolve(worktreeRoot, candidate);
|
|
28
|
+
const root = resolve(worktreeRoot);
|
|
29
|
+
return resolved === root || resolved.startsWith(root + sep);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Approval policy for a worktree child: containment is enforced by code
|
|
33
|
+
* (path checks, deny-list), never by prompt text. There is no interactive
|
|
34
|
+
* fallback — anything outside the policy fails fast (design §11).
|
|
35
|
+
*/
|
|
36
|
+
export class WorktreeApprovalController {
|
|
37
|
+
worktreeRoot;
|
|
38
|
+
constructor(worktreeRoot) {
|
|
39
|
+
this.worktreeRoot = worktreeRoot;
|
|
40
|
+
}
|
|
41
|
+
async request(req) {
|
|
42
|
+
switch (req.type) {
|
|
43
|
+
case "bash": {
|
|
44
|
+
for (const { pattern, reason } of WORKTREE_BASH_DENY_PATTERNS) {
|
|
45
|
+
if (pattern.test(req.command)) {
|
|
46
|
+
return { action: "reject", feedback: `Blocked by worktree policy: ${reason}` };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!isPathInsideWorktree(this.worktreeRoot, req.cwd)) {
|
|
50
|
+
return { action: "reject", feedback: "Blocked by worktree policy: commands must run inside the subagent worktree." };
|
|
51
|
+
}
|
|
52
|
+
// Absolute paths reaching outside the worktree are an escape attempt.
|
|
53
|
+
const absolutePaths = req.command.match(/(?<=^|[\s"'=])\/[^\s"';|&]+/g) ?? [];
|
|
54
|
+
for (const path of absolutePaths) {
|
|
55
|
+
if (path.startsWith("/dev/") || path.startsWith("/tmp/") || path.startsWith("/usr/") || path.startsWith("/bin/") || path.startsWith("/opt/") || path.startsWith("/etc/"))
|
|
56
|
+
continue;
|
|
57
|
+
if (!isPathInsideWorktree(this.worktreeRoot, path)) {
|
|
58
|
+
return {
|
|
59
|
+
action: "reject",
|
|
60
|
+
feedback: `Blocked by worktree policy: the command references a path outside the worktree (${path}).`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { action: "approve" };
|
|
65
|
+
}
|
|
66
|
+
case "edit":
|
|
67
|
+
case "write":
|
|
68
|
+
return isPathInsideWorktree(this.worktreeRoot, req.path)
|
|
69
|
+
? { action: "approve" }
|
|
70
|
+
: { action: "reject", feedback: `Blocked by worktree policy: ${req.path} is outside the subagent worktree.` };
|
|
71
|
+
case "patch":
|
|
72
|
+
return req.paths.every((path) => isPathInsideWorktree(this.worktreeRoot, path))
|
|
73
|
+
? { action: "approve" }
|
|
74
|
+
: { action: "reject", feedback: "Blocked by worktree policy: the patch touches paths outside the subagent worktree." };
|
|
75
|
+
case "lsp":
|
|
76
|
+
return { action: "approve" };
|
|
77
|
+
case "agent_profile":
|
|
78
|
+
return { action: "reject", feedback: "Subagents cannot approve agent profiles." };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
checkRules() {
|
|
82
|
+
return { decision: "ask" };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const WORKTREE_TOOL_NAMES = new Set(["read", "glob", "grep", "edit", "write", "bash"]);
|
|
86
|
+
/**
|
|
87
|
+
* Builds the write child's toolset bound to its worktree: fresh instances
|
|
88
|
+
* with their own FileStateTracker and the worktree approval policy. A
|
|
89
|
+
* profile's tools list can narrow the set but never widen it.
|
|
90
|
+
*/
|
|
91
|
+
export function createWorktreeChildTools(worktreeCwd, include) {
|
|
92
|
+
const approval = new WorktreeApprovalController(worktreeCwd);
|
|
93
|
+
const fileState = new FileStateTracker(worktreeCwd);
|
|
94
|
+
const tools = [
|
|
95
|
+
createReadTool(worktreeCwd, approval, undefined, fileState),
|
|
96
|
+
createGlobTool(worktreeCwd),
|
|
97
|
+
createGrepTool(worktreeCwd),
|
|
98
|
+
createEditTool(worktreeCwd, approval, undefined, fileState),
|
|
99
|
+
createWriteTool(worktreeCwd, {}, approval, undefined, fileState),
|
|
100
|
+
createBashTool(worktreeCwd, approval, fileState),
|
|
101
|
+
];
|
|
102
|
+
if (!include || include.length === 0)
|
|
103
|
+
return tools;
|
|
104
|
+
const requested = new Set(include.filter((name) => WORKTREE_TOOL_NAMES.has(name)));
|
|
105
|
+
return requested.size > 0 ? tools.filter((tool) => requested.has(tool.name)) : tools;
|
|
106
|
+
}
|
package/dist/tools/edit.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* This is the safest way to edit files: old_string must exist exactly once.
|
|
5
5
|
*/
|
|
6
6
|
import type { ApprovalController } from "../approval/types.js";
|
|
7
|
+
import type { CheckpointStore } from "../checkpoints.js";
|
|
7
8
|
import type { ToolRegistryEntry } from "../types.js";
|
|
8
9
|
import { type LspService } from "../lsp/index.js";
|
|
9
10
|
import { type FileStateTracker } from "./file-state.js";
|
|
@@ -14,4 +15,4 @@ export interface EditArgs {
|
|
|
14
15
|
newText: string;
|
|
15
16
|
}>;
|
|
16
17
|
}
|
|
17
|
-
export declare function createEditTool(cwd: string, approval?: ApprovalController, lsp?: LspService, fileState?: FileStateTracker): ToolRegistryEntry;
|
|
18
|
+
export declare function createEditTool(cwd: string, approval?: ApprovalController, lsp?: LspService, fileState?: FileStateTracker, checkpoints?: () => CheckpointStore | undefined): ToolRegistryEntry;
|
package/dist/tools/edit.js
CHANGED
|
@@ -60,7 +60,7 @@ function firstString(...values) {
|
|
|
60
60
|
}
|
|
61
61
|
return undefined;
|
|
62
62
|
}
|
|
63
|
-
export function createEditTool(cwd, approval, lsp, fileState) {
|
|
63
|
+
export function createEditTool(cwd, approval, lsp, fileState, checkpoints) {
|
|
64
64
|
return {
|
|
65
65
|
name: "edit",
|
|
66
66
|
effect: "write_direct",
|
|
@@ -178,6 +178,7 @@ export function createEditTool(cwd, approval, lsp, fileState) {
|
|
|
178
178
|
},
|
|
179
179
|
};
|
|
180
180
|
}
|
|
181
|
+
await checkpoints?.()?.captureBefore(filePath, original);
|
|
181
182
|
await writeFile(filePath, applied.content, "utf-8");
|
|
182
183
|
await fileState?.observe(filePath, "edit", applied.content).catch(() => undefined);
|
|
183
184
|
let output = `Edited ${filePath}${formatEditMatchNotes(applied.matches)}\n\nDiff:\n${diff}`;
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { type LspService } from "../lsp/index.js";
|
|
|
28
28
|
import { type TodoStore } from "./todo.js";
|
|
29
29
|
import { type ToolSearchController } from "./tool-search.js";
|
|
30
30
|
import type { QuestionController } from "../question/index.js";
|
|
31
|
+
import type { CheckpointStore } from "../checkpoints.js";
|
|
31
32
|
import { FileStateTracker } from "./file-state.js";
|
|
32
33
|
export interface CreateAllToolsOptions {
|
|
33
34
|
todoStore?: TodoStore;
|
|
@@ -37,5 +38,11 @@ export interface CreateAllToolsOptions {
|
|
|
37
38
|
toolSearchController?: ToolSearchController;
|
|
38
39
|
lspService?: LspService;
|
|
39
40
|
fileStateTracker?: FileStateTracker;
|
|
41
|
+
/**
|
|
42
|
+
* Lazy accessor for the session's checkpoint store (the session manager may
|
|
43
|
+
* not exist yet when tools are created). Used by edit/write to snapshot
|
|
44
|
+
* files before mutating them so /rewind can restore.
|
|
45
|
+
*/
|
|
46
|
+
checkpoints?: () => CheckpointStore | undefined;
|
|
40
47
|
}
|
|
41
48
|
export declare function createAllTools(cwd: string, skillRegistry?: SkillRegistry, options?: CreateAllToolsOptions): ToolRegistryEntry[];
|
package/dist/tools/index.js
CHANGED
|
@@ -48,8 +48,8 @@ export function createAllTools(cwd, skillRegistry, options = {}) {
|
|
|
48
48
|
createReadTool(cwd, approval, lsp, fileState),
|
|
49
49
|
createBashTool(cwd, approval, fileState),
|
|
50
50
|
...createManagedServerTools(cwd, approval),
|
|
51
|
-
createWriteTool(cwd, {}, approval, lsp, fileState),
|
|
52
|
-
createEditTool(cwd, approval, lsp, fileState),
|
|
51
|
+
createWriteTool(cwd, {}, approval, lsp, fileState, options.checkpoints),
|
|
52
|
+
createEditTool(cwd, approval, lsp, fileState, options.checkpoints),
|
|
53
53
|
createGlobTool(cwd),
|
|
54
54
|
createGrepTool(cwd),
|
|
55
55
|
createLspTool(cwd, lsp, approval),
|
|
@@ -57,7 +57,7 @@ export function createAllTools(cwd, skillRegistry, options = {}) {
|
|
|
57
57
|
createWebFetchTool(approval),
|
|
58
58
|
createMemorySearchTool(cwd),
|
|
59
59
|
createMemoryReadSummaryTool(cwd),
|
|
60
|
-
...createAgentLifecycleTools(),
|
|
60
|
+
...createAgentLifecycleTools({ cwd, approval }),
|
|
61
61
|
...(options.questionController ? [createQuestionTool(options.questionController)] : []),
|
|
62
62
|
...(skillRegistry ? [createSkillSearchTool(skillRegistry), createSkillTool(skillRegistry)] : []),
|
|
63
63
|
...(options.todoStore ? [createTodoTool(options.todoStore)] : []),
|
package/dist/tools/write.d.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Write tool - create files or replace full file contents.
|
|
3
3
|
*/
|
|
4
4
|
import type { ApprovalController } from "../approval/types.js";
|
|
5
|
+
import type { CheckpointStore } from "../checkpoints.js";
|
|
5
6
|
import type { ToolRegistryEntry } from "../types.js";
|
|
6
7
|
import { type LspService } from "../lsp/index.js";
|
|
7
8
|
import { type FileStateTracker } from "./file-state.js";
|
|
8
9
|
export type WriteToolOptions = Record<string, never>;
|
|
9
|
-
export declare function createWriteTool(cwd: string, _options?: WriteToolOptions, approval?: ApprovalController, lsp?: LspService, fileState?: FileStateTracker): ToolRegistryEntry;
|
|
10
|
+
export declare function createWriteTool(cwd: string, _options?: WriteToolOptions, approval?: ApprovalController, lsp?: LspService, fileState?: FileStateTracker, checkpoints?: () => CheckpointStore | undefined): ToolRegistryEntry;
|
package/dist/tools/write.js
CHANGED
|
@@ -18,7 +18,7 @@ function prepareWriteArguments(input) {
|
|
|
18
18
|
}
|
|
19
19
|
return args;
|
|
20
20
|
}
|
|
21
|
-
export function createWriteTool(cwd, _options = {}, approval, lsp, fileState) {
|
|
21
|
+
export function createWriteTool(cwd, _options = {}, approval, lsp, fileState, checkpoints) {
|
|
22
22
|
return {
|
|
23
23
|
name: "write",
|
|
24
24
|
effect: "write_direct",
|
|
@@ -76,6 +76,7 @@ export function createWriteTool(cwd, _options = {}, approval, lsp, fileState) {
|
|
|
76
76
|
return changedDuringApprovalResult(filePath, changed);
|
|
77
77
|
}
|
|
78
78
|
try {
|
|
79
|
+
await checkpoints?.()?.captureBefore(filePath, existed ? oldContent : null);
|
|
79
80
|
await mkdir(dirname(filePath), { recursive: true });
|
|
80
81
|
await writeFile(filePath, args.content, "utf-8");
|
|
81
82
|
await fileState?.observe(filePath, "write", args.content).catch(() => undefined);
|
|
@@ -44,6 +44,12 @@ export declare function isImageFilePath(raw: string): boolean;
|
|
|
44
44
|
export declare function extractImagePathTokens(input: string): ImagePathToken[];
|
|
45
45
|
export declare function removeImagePathTokens(input: string, tokens: ImagePathToken[]): string;
|
|
46
46
|
export declare function imageAttachmentLabel(att: ImageAttachment, index: number): string;
|
|
47
|
+
/**
|
|
48
|
+
* Label for an image path before ingestion runs. Matches what
|
|
49
|
+
* imageAttachmentLabel produces for the same file, so a label inserted at
|
|
50
|
+
* paste time stays a valid key once the attachment is registered.
|
|
51
|
+
*/
|
|
52
|
+
export declare function imageLabelForPath(rawPath: string, index: number): string;
|
|
47
53
|
export declare function imageAttachmentReference(att: ImageAttachment, index: number): string;
|
|
48
54
|
export declare function imageAttachmentLabelPattern(): RegExp;
|
|
49
55
|
export declare function buildImageContentParts(promptText: string, attachments: ImageAttachment[]): ContentPart[];
|
|
@@ -61,6 +67,18 @@ export declare function buildImageContentPartsFromLabels(input: string, attachme
|
|
|
61
67
|
* only on a space that is followed by the start of a new absolute path.
|
|
62
68
|
*/
|
|
63
69
|
export declare function splitPastedPaths(pasted: string): string[];
|
|
70
|
+
/**
|
|
71
|
+
* True when a pasted blob consists solely of image file paths (drag from
|
|
72
|
+
* Finder, or a terminal that converts clipboard images to temp-file paths).
|
|
73
|
+
*/
|
|
74
|
+
export declare function isImagePathPaste(pasted: string): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Bare image filename with no directory, e.g. "Screenshot ... AM.png".
|
|
77
|
+
* Copying an image file in Finder puts only the file's NAME in the
|
|
78
|
+
* clipboard's plain-text flavor — the actual bits arrive as a file-url or
|
|
79
|
+
* image flavor that must be read from the clipboard separately.
|
|
80
|
+
*/
|
|
81
|
+
export declare function bareImageFilenameFromPaste(pasted: string): string | null;
|
|
64
82
|
export declare function readImageFromPath(rawPath: string): Promise<ImageAttachment | null>;
|
|
65
83
|
/** macOS screenshot shortcut writes to these paths and they may be auto-cleaned. */
|
|
66
84
|
export declare function isScreenshotTempPath(s: string): boolean;
|
package/dist/tui/image-paste.js
CHANGED
|
@@ -65,6 +65,15 @@ export function removeImagePathTokens(input, tokens) {
|
|
|
65
65
|
export function imageAttachmentLabel(att, index) {
|
|
66
66
|
return `image#${index}${imageExtension(att)}`;
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Label for an image path before ingestion runs. Matches what
|
|
70
|
+
* imageAttachmentLabel produces for the same file, so a label inserted at
|
|
71
|
+
* paste time stays a valid key once the attachment is registered.
|
|
72
|
+
*/
|
|
73
|
+
export function imageLabelForPath(rawPath, index) {
|
|
74
|
+
const ext = path.extname(unescapeShell(rawPath.trim())).toLowerCase() || ".png";
|
|
75
|
+
return `image#${index}${ext}`;
|
|
76
|
+
}
|
|
68
77
|
export function imageAttachmentReference(att, index) {
|
|
69
78
|
return `[${imageAttachmentLabel(att, index)}]`;
|
|
70
79
|
}
|
|
@@ -155,6 +164,30 @@ export function splitPastedPaths(pasted) {
|
|
|
155
164
|
}
|
|
156
165
|
return out;
|
|
157
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* True when a pasted blob consists solely of image file paths (drag from
|
|
169
|
+
* Finder, or a terminal that converts clipboard images to temp-file paths).
|
|
170
|
+
*/
|
|
171
|
+
export function isImagePathPaste(pasted) {
|
|
172
|
+
const pieces = splitPastedPaths(pasted);
|
|
173
|
+
return pieces.length > 0 && pieces.every((piece) => isImageFilePath(piece));
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Bare image filename with no directory, e.g. "Screenshot ... AM.png".
|
|
177
|
+
* Copying an image file in Finder puts only the file's NAME in the
|
|
178
|
+
* clipboard's plain-text flavor — the actual bits arrive as a file-url or
|
|
179
|
+
* image flavor that must be read from the clipboard separately.
|
|
180
|
+
*/
|
|
181
|
+
export function bareImageFilenameFromPaste(pasted) {
|
|
182
|
+
const s = pasted.trim();
|
|
183
|
+
if (!s || s.length > 255)
|
|
184
|
+
return null;
|
|
185
|
+
if (/[\n\r/\\]/.test(s))
|
|
186
|
+
return null;
|
|
187
|
+
if (!IMAGE_EXT.test(s))
|
|
188
|
+
return null;
|
|
189
|
+
return s;
|
|
190
|
+
}
|
|
158
191
|
function mediaTypeFromExt(p) {
|
|
159
192
|
const ext = path.extname(p).toLowerCase();
|
|
160
193
|
if (ext === ".jpg" || ext === ".jpeg")
|
|
@@ -384,6 +417,14 @@ export async function ingestImagePath(p) {
|
|
|
384
417
|
return { attachment: sized };
|
|
385
418
|
}
|
|
386
419
|
export async function ingestClipboardImage() {
|
|
420
|
+
// A file reference wins over bitmap flavors: for a copied FILE, coercing
|
|
421
|
+
// the clipboard to PNGf yields the file's generic ICON, not the image.
|
|
422
|
+
const filePath = await getClipboardFilePath();
|
|
423
|
+
if (filePath) {
|
|
424
|
+
if (isImageFilePath(filePath))
|
|
425
|
+
return ingestImagePath(filePath);
|
|
426
|
+
return { error: `clipboard file is not an image: ${filePath}` };
|
|
427
|
+
}
|
|
387
428
|
const raw = await getImageFromClipboard();
|
|
388
429
|
if (!raw)
|
|
389
430
|
return { error: "clipboard has no image" };
|
|
@@ -393,6 +434,25 @@ export async function ingestClipboardImage() {
|
|
|
393
434
|
return { error: validation.reason };
|
|
394
435
|
return { attachment: sized };
|
|
395
436
|
}
|
|
437
|
+
async function getClipboardFilePath() {
|
|
438
|
+
if (process.platform !== "darwin")
|
|
439
|
+
return null;
|
|
440
|
+
try {
|
|
441
|
+
// Probe first — AppleScript happily coerces plain TEXT into a file URL,
|
|
442
|
+
// so only trust «class furl» when the clipboard really carries one.
|
|
443
|
+
const probe = await execFileAsync("osascript", ["-e", "clipboard info for «class furl»"], {
|
|
444
|
+
timeout: 5000,
|
|
445
|
+
});
|
|
446
|
+
if (!String(probe.stdout).includes("furl"))
|
|
447
|
+
return null;
|
|
448
|
+
const result = await execFileAsync("osascript", ["-e", "POSIX path of (the clipboard as «class furl»)"], { timeout: 5000 });
|
|
449
|
+
const p = String(result.stdout).trim();
|
|
450
|
+
return p || null;
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
396
456
|
export async function resolveImageInput(input, options = {}) {
|
|
397
457
|
const tokens = extractImagePathTokens(input);
|
|
398
458
|
if (tokens.length === 0) {
|
package/dist/tui/run.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type Agent } from "../agent.js";
|
|
|
2
2
|
import type { CliArgs } from "../cli.js";
|
|
3
3
|
import type { ThemeMode } from "../config.js";
|
|
4
4
|
import type { ExternalHookController } from "../hooks/controller.js";
|
|
5
|
-
import
|
|
5
|
+
import { SessionManager } from "../session.js";
|
|
6
6
|
import type { PlanDecision, Provider } from "../types.js";
|
|
7
7
|
import type { ProviderRegistry } from "../provider-registry.js";
|
|
8
8
|
import type { SkillRegistry } from "../skills/registry.js";
|
|
@@ -45,5 +45,15 @@ export interface RunTuiOptions {
|
|
|
45
45
|
runMemoryRefresh?: (scope?: MemoryScope) => Promise<string>;
|
|
46
46
|
/** One-line "update available" notice shown on the home screen, if any. */
|
|
47
47
|
updateNotice?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Swap the active session in place (driven by the /session picker).
|
|
50
|
+
* Rebinds persistence to the picked session file and replaces the agent's
|
|
51
|
+
* message history; the TUI rebuilds its transcript from the result.
|
|
52
|
+
*/
|
|
53
|
+
switchSession?: (sessionFile: string) => {
|
|
54
|
+
manager: SessionManager;
|
|
55
|
+
} | {
|
|
56
|
+
error: string;
|
|
57
|
+
};
|
|
48
58
|
}
|
|
49
59
|
export declare function runTui(agent: Agent, args: CliArgs, options?: RunTuiOptions): Promise<void>;
|