@dreb/coding-agent 2.22.1 → 2.23.0
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/CHANGELOG.md +4 -0
- package/README.md +1 -1
- package/dist/core/keybindings.d.ts +5 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +4 -0
- package/dist/core/keybindings.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/components/copy-selector.d.ts +32 -0
- package/dist/modes/interactive/components/copy-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/copy-selector.js +155 -0
- package/dist/modes/interactive/components/copy-selector.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +62 -9
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/clipboard.d.ts +4 -1
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +12 -2
- package/dist/utils/clipboard.js.map +1 -1
- package/dist/utils/message-text.d.ts +21 -0
- package/dist/utils/message-text.d.ts.map +1 -0
- package/dist/utils/message-text.js +118 -0
- package/dist/utils/message-text.js.map +1 -0
- package/docs/keybindings.md +1 -0
- package/package.json +1 -1
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type ClipboardResult = {
|
|
2
|
+
method: "native" | "platform" | "osc52";
|
|
3
|
+
};
|
|
4
|
+
export declare function copyToClipboard(text: string): Promise<ClipboardResult>;
|
|
2
5
|
//# sourceMappingURL=clipboard.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clipboard.d.ts","sourceRoot":"","sources":["../../src/utils/clipboard.ts"],"names":[],"mappings":"AAoBA,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"clipboard.d.ts","sourceRoot":"","sources":["../../src/utils/clipboard.ts"],"names":[],"mappings":"AAoBA,MAAM,MAAM,eAAe,GAAG;IAAE,MAAM,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAA;CAAE,CAAC;AAE1E,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAyE5E","sourcesContent":["import { execSync, spawn } from \"child_process\";\nimport { platform } from \"os\";\nimport { isWaylandSession } from \"./clipboard-image.js\";\nimport { clipboard } from \"./clipboard-native.js\";\n\ntype NativeClipboardExecOptions = {\n\tinput: string;\n\ttimeout: number;\n\tstdio: [\"pipe\", \"ignore\", \"ignore\"];\n};\n\nfunction copyToX11Clipboard(options: NativeClipboardExecOptions): void {\n\ttry {\n\t\texecSync(\"xclip -selection clipboard\", options);\n\t} catch {\n\t\t// xclip unavailable — fall back to xsel\n\t\texecSync(\"xsel --clipboard --input\", options);\n\t}\n}\n\nexport type ClipboardResult = { method: \"native\" | \"platform\" | \"osc52\" };\n\nexport async function copyToClipboard(text: string): Promise<ClipboardResult> {\n\t// Always emit OSC 52 - works over SSH/mosh, harmless locally\n\tconst encoded = Buffer.from(text).toString(\"base64\");\n\tprocess.stdout.write(`\\x1b]52;c;${encoded}\\x07`);\n\n\ttry {\n\t\tif (clipboard) {\n\t\t\tawait clipboard.setText(text);\n\t\t\treturn { method: \"native\" };\n\t\t}\n\t} catch {\n\t\t/* Native clipboard module threw — fall through to platform-specific tools */\n\t}\n\n\t// Also try native tools (best effort for local sessions)\n\tconst p = platform();\n\tconst options: NativeClipboardExecOptions = { input: text, timeout: 5000, stdio: [\"pipe\", \"ignore\", \"ignore\"] };\n\n\ttry {\n\t\tif (p === \"darwin\") {\n\t\t\texecSync(\"pbcopy\", options);\n\t\t\treturn { method: \"platform\" };\n\t\t} else if (p === \"win32\") {\n\t\t\texecSync(\"clip\", options);\n\t\t\treturn { method: \"platform\" };\n\t\t} else {\n\t\t\t// Linux. Try Termux, Wayland, or X11 clipboard tools.\n\t\t\tif (process.env.TERMUX_VERSION) {\n\t\t\t\ttry {\n\t\t\t\t\texecSync(\"termux-clipboard-set\", options);\n\t\t\t\t\treturn { method: \"platform\" };\n\t\t\t\t} catch {\n\t\t\t\t\t/* termux-clipboard-set unavailable — fall back to Wayland or X11 tools */\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst hasWaylandDisplay = Boolean(process.env.WAYLAND_DISPLAY);\n\t\t\tconst hasX11Display = Boolean(process.env.DISPLAY);\n\t\t\tconst isWayland = isWaylandSession();\n\t\t\tif (isWayland && hasWaylandDisplay) {\n\t\t\t\ttry {\n\t\t\t\t\t// Verify wl-copy exists (spawn errors are async and won't be caught)\n\t\t\t\t\texecSync(\"which wl-copy\", { stdio: \"ignore\" });\n\t\t\t\t\t// wl-copy with execSync hangs due to fork behavior; use spawn instead\n\t\t\t\t\tconst proc = spawn(\"wl-copy\", [], { stdio: [\"pipe\", \"ignore\", \"ignore\"] });\n\t\t\t\t\tproc.on(\"error\", () => {\n\t\t\t\t\t\t// Spawn failed after which check (TOCTOU, permissions, etc.)\n\t\t\t\t\t});\n\t\t\t\t\tproc.stdin.on(\"error\", () => {\n\t\t\t\t\t\t// Ignore EPIPE errors if wl-copy exits early\n\t\t\t\t\t});\n\t\t\t\t\tproc.stdin.write(text);\n\t\t\t\t\tproc.stdin.end();\n\t\t\t\t\tproc.unref();\n\t\t\t\t\t// Can't confirm wl-copy succeeded before unref — report osc52 (already emitted above)\n\t\t\t\t\treturn { method: \"osc52\" };\n\t\t\t\t} catch {\n\t\t\t\t\t/* wl-copy unavailable or failed — fall back to X11 if available */\n\t\t\t\t\tif (hasX11Display) {\n\t\t\t\t\t\tcopyToX11Clipboard(options);\n\t\t\t\t\t\treturn { method: \"platform\" };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (hasX11Display) {\n\t\t\t\tcopyToX11Clipboard(options);\n\t\t\t\treturn { method: \"platform\" };\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t/* Platform clipboard tools failed — OSC 52 already emitted above as fallback */\n\t}\n\n\treturn { method: \"osc52\" };\n}\n"]}
|
package/dist/utils/clipboard.js
CHANGED
|
@@ -18,7 +18,7 @@ export async function copyToClipboard(text) {
|
|
|
18
18
|
try {
|
|
19
19
|
if (clipboard) {
|
|
20
20
|
await clipboard.setText(text);
|
|
21
|
-
return;
|
|
21
|
+
return { method: "native" };
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
catch {
|
|
@@ -30,16 +30,18 @@ export async function copyToClipboard(text) {
|
|
|
30
30
|
try {
|
|
31
31
|
if (p === "darwin") {
|
|
32
32
|
execSync("pbcopy", options);
|
|
33
|
+
return { method: "platform" };
|
|
33
34
|
}
|
|
34
35
|
else if (p === "win32") {
|
|
35
36
|
execSync("clip", options);
|
|
37
|
+
return { method: "platform" };
|
|
36
38
|
}
|
|
37
39
|
else {
|
|
38
40
|
// Linux. Try Termux, Wayland, or X11 clipboard tools.
|
|
39
41
|
if (process.env.TERMUX_VERSION) {
|
|
40
42
|
try {
|
|
41
43
|
execSync("termux-clipboard-set", options);
|
|
42
|
-
return;
|
|
44
|
+
return { method: "platform" };
|
|
43
45
|
}
|
|
44
46
|
catch {
|
|
45
47
|
/* termux-clipboard-set unavailable — fall back to Wayland or X11 tools */
|
|
@@ -54,27 +56,35 @@ export async function copyToClipboard(text) {
|
|
|
54
56
|
execSync("which wl-copy", { stdio: "ignore" });
|
|
55
57
|
// wl-copy with execSync hangs due to fork behavior; use spawn instead
|
|
56
58
|
const proc = spawn("wl-copy", [], { stdio: ["pipe", "ignore", "ignore"] });
|
|
59
|
+
proc.on("error", () => {
|
|
60
|
+
// Spawn failed after which check (TOCTOU, permissions, etc.)
|
|
61
|
+
});
|
|
57
62
|
proc.stdin.on("error", () => {
|
|
58
63
|
// Ignore EPIPE errors if wl-copy exits early
|
|
59
64
|
});
|
|
60
65
|
proc.stdin.write(text);
|
|
61
66
|
proc.stdin.end();
|
|
62
67
|
proc.unref();
|
|
68
|
+
// Can't confirm wl-copy succeeded before unref — report osc52 (already emitted above)
|
|
69
|
+
return { method: "osc52" };
|
|
63
70
|
}
|
|
64
71
|
catch {
|
|
65
72
|
/* wl-copy unavailable or failed — fall back to X11 if available */
|
|
66
73
|
if (hasX11Display) {
|
|
67
74
|
copyToX11Clipboard(options);
|
|
75
|
+
return { method: "platform" };
|
|
68
76
|
}
|
|
69
77
|
}
|
|
70
78
|
}
|
|
71
79
|
else if (hasX11Display) {
|
|
72
80
|
copyToX11Clipboard(options);
|
|
81
|
+
return { method: "platform" };
|
|
73
82
|
}
|
|
74
83
|
}
|
|
75
84
|
}
|
|
76
85
|
catch {
|
|
77
86
|
/* Platform clipboard tools failed — OSC 52 already emitted above as fallback */
|
|
78
87
|
}
|
|
88
|
+
return { method: "osc52" };
|
|
79
89
|
}
|
|
80
90
|
//# sourceMappingURL=clipboard.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clipboard.js","sourceRoot":"","sources":["../../src/utils/clipboard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AAC9B,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAQlD,SAAS,kBAAkB,CAAC,OAAmC,EAAQ;IACtE,IAAI,CAAC;QACJ,QAAQ,CAAC,4BAA4B,EAAE,OAAO,CAAC,CAAC;IACjD,CAAC;IAAC,MAAM,CAAC;QACR,0CAAwC;QACxC,QAAQ,CAAC,0BAA0B,EAAE,OAAO,CAAC,CAAC;IAC/C,CAAC;AAAA,CACD;
|
|
1
|
+
{"version":3,"file":"clipboard.js","sourceRoot":"","sources":["../../src/utils/clipboard.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawn } from \"child_process\";\nimport { platform } from \"os\";\nimport { isWaylandSession } from \"./clipboard-image.js\";\nimport { clipboard } from \"./clipboard-native.js\";\n\ntype NativeClipboardExecOptions = {\n\tinput: string;\n\ttimeout: number;\n\tstdio: [\"pipe\", \"ignore\", \"ignore\"];\n};\n\nfunction copyToX11Clipboard(options: NativeClipboardExecOptions): void {\n\ttry {\n\t\texecSync(\"xclip -selection clipboard\", options);\n\t} catch {\n\t\t// xclip unavailable — fall back to xsel\n\t\texecSync(\"xsel --clipboard --input\", options);\n\t}\n}\n\nexport type ClipboardResult = { method: \"native\" | \"platform\" | \"osc52\" };\n\nexport async function copyToClipboard(text: string): Promise<ClipboardResult> {\n\t// Always emit OSC 52 - works over SSH/mosh, harmless locally\n\tconst encoded = Buffer.from(text).toString(\"base64\");\n\tprocess.stdout.write(`\\x1b]52;c;${encoded}\\x07`);\n\n\ttry {\n\t\tif (clipboard) {\n\t\t\tawait clipboard.setText(text);\n\t\t\treturn { method: \"native\" };\n\t\t}\n\t} catch {\n\t\t/* Native clipboard module threw — fall through to platform-specific tools */\n\t}\n\n\t// Also try native tools (best effort for local sessions)\n\tconst p = platform();\n\tconst options: NativeClipboardExecOptions = { input: text, timeout: 5000, stdio: [\"pipe\", \"ignore\", \"ignore\"] };\n\n\ttry {\n\t\tif (p === \"darwin\") {\n\t\t\texecSync(\"pbcopy\", options);\n\t\t\treturn { method: \"platform\" };\n\t\t} else if (p === \"win32\") {\n\t\t\texecSync(\"clip\", options);\n\t\t\treturn { method: \"platform\" };\n\t\t} else {\n\t\t\t// Linux. Try Termux, Wayland, or X11 clipboard tools.\n\t\t\tif (process.env.TERMUX_VERSION) {\n\t\t\t\ttry {\n\t\t\t\t\texecSync(\"termux-clipboard-set\", options);\n\t\t\t\t\treturn { method: \"platform\" };\n\t\t\t\t} catch {\n\t\t\t\t\t/* termux-clipboard-set unavailable — fall back to Wayland or X11 tools */\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst hasWaylandDisplay = Boolean(process.env.WAYLAND_DISPLAY);\n\t\t\tconst hasX11Display = Boolean(process.env.DISPLAY);\n\t\t\tconst isWayland = isWaylandSession();\n\t\t\tif (isWayland && hasWaylandDisplay) {\n\t\t\t\ttry {\n\t\t\t\t\t// Verify wl-copy exists (spawn errors are async and won't be caught)\n\t\t\t\t\texecSync(\"which wl-copy\", { stdio: \"ignore\" });\n\t\t\t\t\t// wl-copy with execSync hangs due to fork behavior; use spawn instead\n\t\t\t\t\tconst proc = spawn(\"wl-copy\", [], { stdio: [\"pipe\", \"ignore\", \"ignore\"] });\n\t\t\t\t\tproc.on(\"error\", () => {\n\t\t\t\t\t\t// Spawn failed after which check (TOCTOU, permissions, etc.)\n\t\t\t\t\t});\n\t\t\t\t\tproc.stdin.on(\"error\", () => {\n\t\t\t\t\t\t// Ignore EPIPE errors if wl-copy exits early\n\t\t\t\t\t});\n\t\t\t\t\tproc.stdin.write(text);\n\t\t\t\t\tproc.stdin.end();\n\t\t\t\t\tproc.unref();\n\t\t\t\t\t// Can't confirm wl-copy succeeded before unref — report osc52 (already emitted above)\n\t\t\t\t\treturn { method: \"osc52\" };\n\t\t\t\t} catch {\n\t\t\t\t\t/* wl-copy unavailable or failed — fall back to X11 if available */\n\t\t\t\t\tif (hasX11Display) {\n\t\t\t\t\t\tcopyToX11Clipboard(options);\n\t\t\t\t\t\treturn { method: \"platform\" };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (hasX11Display) {\n\t\t\t\tcopyToX11Clipboard(options);\n\t\t\t\treturn { method: \"platform\" };\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t/* Platform clipboard tools failed — OSC 52 already emitted above as fallback */\n\t}\n\n\treturn { method: \"osc52\" };\n}\n"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for extracting copyable plain text from AgentMessages.
|
|
3
|
+
* Used by the copy selector modal to present message content for clipboard copy.
|
|
4
|
+
*/
|
|
5
|
+
import type { AgentMessage } from "@dreb/agent-core";
|
|
6
|
+
/**
|
|
7
|
+
* Extract copyable plain text from any AgentMessage.
|
|
8
|
+
* Returns the original source text without any terminal wrapping or ANSI codes.
|
|
9
|
+
* Returns empty string for messages with no meaningful text content (e.g., image-only).
|
|
10
|
+
*/
|
|
11
|
+
export declare function extractCopyableText(message: AgentMessage): string;
|
|
12
|
+
/**
|
|
13
|
+
* Get a short label for a message's role (used in the copy selector UI).
|
|
14
|
+
*/
|
|
15
|
+
export declare function getMessageRoleLabel(message: AgentMessage): string;
|
|
16
|
+
/**
|
|
17
|
+
* Get a single-line preview of a message's content (for display in selector).
|
|
18
|
+
* Returns plain text, no ANSI, truncated to ~200 chars. Caller handles width truncation.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getMessagePreview(message: AgentMessage): string;
|
|
21
|
+
//# sourceMappingURL=message-text.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-text.d.ts","sourceRoot":"","sources":["../../src/utils/message-text.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AASrD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAmBjE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAmBjE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAW/D","sourcesContent":["/**\n * Utilities for extracting copyable plain text from AgentMessages.\n * Used by the copy selector modal to present message content for clipboard copy.\n */\n\nimport type { AgentMessage } from \"@dreb/agent-core\";\nimport type { AssistantMessage, TextContent, ToolResultMessage, UserMessage } from \"@dreb/ai\";\nimport type {\n\tBashExecutionMessage,\n\tBranchSummaryMessage,\n\tCompactionSummaryMessage,\n\tCustomMessage,\n} from \"../core/messages.js\";\n\n/**\n * Extract copyable plain text from any AgentMessage.\n * Returns the original source text without any terminal wrapping or ANSI codes.\n * Returns empty string for messages with no meaningful text content (e.g., image-only).\n */\nexport function extractCopyableText(message: AgentMessage): string {\n\tswitch (message.role) {\n\t\tcase \"user\":\n\t\t\treturn extractUserText(message);\n\t\tcase \"assistant\":\n\t\t\treturn extractAssistantText(message);\n\t\tcase \"toolResult\":\n\t\t\treturn extractToolResultText(message);\n\t\tcase \"bashExecution\":\n\t\t\treturn extractBashText(message);\n\t\tcase \"branchSummary\":\n\t\t\treturn (message as BranchSummaryMessage).summary;\n\t\tcase \"compactionSummary\":\n\t\t\treturn (message as CompactionSummaryMessage).summary;\n\t\tcase \"custom\":\n\t\t\treturn extractCustomText(message as CustomMessage);\n\t\tdefault:\n\t\t\treturn \"\";\n\t}\n}\n\n/**\n * Get a short label for a message's role (used in the copy selector UI).\n */\nexport function getMessageRoleLabel(message: AgentMessage): string {\n\tswitch (message.role) {\n\t\tcase \"user\":\n\t\t\treturn \"You\";\n\t\tcase \"assistant\":\n\t\t\treturn \"Assistant\";\n\t\tcase \"toolResult\":\n\t\t\treturn `Tool: ${(message as ToolResultMessage).toolName}`;\n\t\tcase \"bashExecution\":\n\t\t\treturn \"Bash\";\n\t\tcase \"branchSummary\":\n\t\t\treturn \"Branch\";\n\t\tcase \"compactionSummary\":\n\t\t\treturn \"Summary\";\n\t\tcase \"custom\":\n\t\t\treturn `Custom: ${(message as CustomMessage).customType}`;\n\t\tdefault:\n\t\t\treturn \"Unknown\";\n\t}\n}\n\n/**\n * Get a single-line preview of a message's content (for display in selector).\n * Returns plain text, no ANSI, truncated to ~200 chars. Caller handles width truncation.\n */\nexport function getMessagePreview(message: AgentMessage): string {\n\tconst text = extractCopyableText(message);\n\tif (!text) {\n\t\treturn \"[no text content]\";\n\t}\n\t// Normalize to single line: replace newlines with spaces, collapse whitespace\n\tconst singleLine = text.replace(/\\n/g, \" \").replace(/\\s+/g, \" \").trim();\n\tif (singleLine.length <= 200) {\n\t\treturn singleLine;\n\t}\n\treturn singleLine.slice(0, 200);\n}\n\n// --- Internal helpers ---\n\nfunction extractTextBlocks(content: (TextContent | { type: string })[]): string {\n\treturn content\n\t\t.filter((block): block is TextContent => block.type === \"text\")\n\t\t.map((block) => block.text)\n\t\t.join(\"\\n\");\n}\n\nfunction extractUserText(message: UserMessage): string {\n\tif (typeof message.content === \"string\") {\n\t\treturn message.content;\n\t}\n\treturn extractTextBlocks(message.content);\n}\n\nfunction extractAssistantText(message: AssistantMessage): string {\n\tconst parts: string[] = [];\n\n\t// Collect text blocks\n\tconst textParts = message.content\n\t\t.filter((block): block is TextContent => block.type === \"text\")\n\t\t.map((block) => block.text);\n\n\tif (textParts.length > 0) {\n\t\tparts.push(textParts.join(\"\\n\"));\n\t}\n\n\t// Collect thinking blocks with header\n\tfor (const block of message.content) {\n\t\tif (block.type === \"thinking\" && \"thinking\" in block) {\n\t\t\tparts.push(`[thinking]\\n${block.thinking}`);\n\t\t}\n\t}\n\n\treturn parts.join(\"\\n\");\n}\n\nfunction extractToolResultText(message: ToolResultMessage): string {\n\tconst textContent = extractTextBlocks(message.content);\n\tif (!textContent) {\n\t\treturn \"\";\n\t}\n\treturn `[${message.toolName}]\\n${textContent}`;\n}\n\nfunction extractBashText(message: BashExecutionMessage): string {\n\treturn `$ ${message.command}\\n${message.output}`;\n}\n\nfunction extractCustomText(message: CustomMessage): string {\n\tif (typeof message.content === \"string\") {\n\t\treturn message.content;\n\t}\n\tif (Array.isArray(message.content)) {\n\t\treturn extractTextBlocks(message.content);\n\t}\n\treturn \"\";\n}\n"]}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for extracting copyable plain text from AgentMessages.
|
|
3
|
+
* Used by the copy selector modal to present message content for clipboard copy.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Extract copyable plain text from any AgentMessage.
|
|
7
|
+
* Returns the original source text without any terminal wrapping or ANSI codes.
|
|
8
|
+
* Returns empty string for messages with no meaningful text content (e.g., image-only).
|
|
9
|
+
*/
|
|
10
|
+
export function extractCopyableText(message) {
|
|
11
|
+
switch (message.role) {
|
|
12
|
+
case "user":
|
|
13
|
+
return extractUserText(message);
|
|
14
|
+
case "assistant":
|
|
15
|
+
return extractAssistantText(message);
|
|
16
|
+
case "toolResult":
|
|
17
|
+
return extractToolResultText(message);
|
|
18
|
+
case "bashExecution":
|
|
19
|
+
return extractBashText(message);
|
|
20
|
+
case "branchSummary":
|
|
21
|
+
return message.summary;
|
|
22
|
+
case "compactionSummary":
|
|
23
|
+
return message.summary;
|
|
24
|
+
case "custom":
|
|
25
|
+
return extractCustomText(message);
|
|
26
|
+
default:
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Get a short label for a message's role (used in the copy selector UI).
|
|
32
|
+
*/
|
|
33
|
+
export function getMessageRoleLabel(message) {
|
|
34
|
+
switch (message.role) {
|
|
35
|
+
case "user":
|
|
36
|
+
return "You";
|
|
37
|
+
case "assistant":
|
|
38
|
+
return "Assistant";
|
|
39
|
+
case "toolResult":
|
|
40
|
+
return `Tool: ${message.toolName}`;
|
|
41
|
+
case "bashExecution":
|
|
42
|
+
return "Bash";
|
|
43
|
+
case "branchSummary":
|
|
44
|
+
return "Branch";
|
|
45
|
+
case "compactionSummary":
|
|
46
|
+
return "Summary";
|
|
47
|
+
case "custom":
|
|
48
|
+
return `Custom: ${message.customType}`;
|
|
49
|
+
default:
|
|
50
|
+
return "Unknown";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get a single-line preview of a message's content (for display in selector).
|
|
55
|
+
* Returns plain text, no ANSI, truncated to ~200 chars. Caller handles width truncation.
|
|
56
|
+
*/
|
|
57
|
+
export function getMessagePreview(message) {
|
|
58
|
+
const text = extractCopyableText(message);
|
|
59
|
+
if (!text) {
|
|
60
|
+
return "[no text content]";
|
|
61
|
+
}
|
|
62
|
+
// Normalize to single line: replace newlines with spaces, collapse whitespace
|
|
63
|
+
const singleLine = text.replace(/\n/g, " ").replace(/\s+/g, " ").trim();
|
|
64
|
+
if (singleLine.length <= 200) {
|
|
65
|
+
return singleLine;
|
|
66
|
+
}
|
|
67
|
+
return singleLine.slice(0, 200);
|
|
68
|
+
}
|
|
69
|
+
// --- Internal helpers ---
|
|
70
|
+
function extractTextBlocks(content) {
|
|
71
|
+
return content
|
|
72
|
+
.filter((block) => block.type === "text")
|
|
73
|
+
.map((block) => block.text)
|
|
74
|
+
.join("\n");
|
|
75
|
+
}
|
|
76
|
+
function extractUserText(message) {
|
|
77
|
+
if (typeof message.content === "string") {
|
|
78
|
+
return message.content;
|
|
79
|
+
}
|
|
80
|
+
return extractTextBlocks(message.content);
|
|
81
|
+
}
|
|
82
|
+
function extractAssistantText(message) {
|
|
83
|
+
const parts = [];
|
|
84
|
+
// Collect text blocks
|
|
85
|
+
const textParts = message.content
|
|
86
|
+
.filter((block) => block.type === "text")
|
|
87
|
+
.map((block) => block.text);
|
|
88
|
+
if (textParts.length > 0) {
|
|
89
|
+
parts.push(textParts.join("\n"));
|
|
90
|
+
}
|
|
91
|
+
// Collect thinking blocks with header
|
|
92
|
+
for (const block of message.content) {
|
|
93
|
+
if (block.type === "thinking" && "thinking" in block) {
|
|
94
|
+
parts.push(`[thinking]\n${block.thinking}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return parts.join("\n");
|
|
98
|
+
}
|
|
99
|
+
function extractToolResultText(message) {
|
|
100
|
+
const textContent = extractTextBlocks(message.content);
|
|
101
|
+
if (!textContent) {
|
|
102
|
+
return "";
|
|
103
|
+
}
|
|
104
|
+
return `[${message.toolName}]\n${textContent}`;
|
|
105
|
+
}
|
|
106
|
+
function extractBashText(message) {
|
|
107
|
+
return `$ ${message.command}\n${message.output}`;
|
|
108
|
+
}
|
|
109
|
+
function extractCustomText(message) {
|
|
110
|
+
if (typeof message.content === "string") {
|
|
111
|
+
return message.content;
|
|
112
|
+
}
|
|
113
|
+
if (Array.isArray(message.content)) {
|
|
114
|
+
return extractTextBlocks(message.content);
|
|
115
|
+
}
|
|
116
|
+
return "";
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=message-text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-text.js","sourceRoot":"","sources":["../../src/utils/message-text.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Utilities for extracting copyable plain text from AgentMessages.\n * Used by the copy selector modal to present message content for clipboard copy.\n */\n\nimport type { AgentMessage } from \"@dreb/agent-core\";\nimport type { AssistantMessage, TextContent, ToolResultMessage, UserMessage } from \"@dreb/ai\";\nimport type {\n\tBashExecutionMessage,\n\tBranchSummaryMessage,\n\tCompactionSummaryMessage,\n\tCustomMessage,\n} from \"../core/messages.js\";\n\n/**\n * Extract copyable plain text from any AgentMessage.\n * Returns the original source text without any terminal wrapping or ANSI codes.\n * Returns empty string for messages with no meaningful text content (e.g., image-only).\n */\nexport function extractCopyableText(message: AgentMessage): string {\n\tswitch (message.role) {\n\t\tcase \"user\":\n\t\t\treturn extractUserText(message);\n\t\tcase \"assistant\":\n\t\t\treturn extractAssistantText(message);\n\t\tcase \"toolResult\":\n\t\t\treturn extractToolResultText(message);\n\t\tcase \"bashExecution\":\n\t\t\treturn extractBashText(message);\n\t\tcase \"branchSummary\":\n\t\t\treturn (message as BranchSummaryMessage).summary;\n\t\tcase \"compactionSummary\":\n\t\t\treturn (message as CompactionSummaryMessage).summary;\n\t\tcase \"custom\":\n\t\t\treturn extractCustomText(message as CustomMessage);\n\t\tdefault:\n\t\t\treturn \"\";\n\t}\n}\n\n/**\n * Get a short label for a message's role (used in the copy selector UI).\n */\nexport function getMessageRoleLabel(message: AgentMessage): string {\n\tswitch (message.role) {\n\t\tcase \"user\":\n\t\t\treturn \"You\";\n\t\tcase \"assistant\":\n\t\t\treturn \"Assistant\";\n\t\tcase \"toolResult\":\n\t\t\treturn `Tool: ${(message as ToolResultMessage).toolName}`;\n\t\tcase \"bashExecution\":\n\t\t\treturn \"Bash\";\n\t\tcase \"branchSummary\":\n\t\t\treturn \"Branch\";\n\t\tcase \"compactionSummary\":\n\t\t\treturn \"Summary\";\n\t\tcase \"custom\":\n\t\t\treturn `Custom: ${(message as CustomMessage).customType}`;\n\t\tdefault:\n\t\t\treturn \"Unknown\";\n\t}\n}\n\n/**\n * Get a single-line preview of a message's content (for display in selector).\n * Returns plain text, no ANSI, truncated to ~200 chars. Caller handles width truncation.\n */\nexport function getMessagePreview(message: AgentMessage): string {\n\tconst text = extractCopyableText(message);\n\tif (!text) {\n\t\treturn \"[no text content]\";\n\t}\n\t// Normalize to single line: replace newlines with spaces, collapse whitespace\n\tconst singleLine = text.replace(/\\n/g, \" \").replace(/\\s+/g, \" \").trim();\n\tif (singleLine.length <= 200) {\n\t\treturn singleLine;\n\t}\n\treturn singleLine.slice(0, 200);\n}\n\n// --- Internal helpers ---\n\nfunction extractTextBlocks(content: (TextContent | { type: string })[]): string {\n\treturn content\n\t\t.filter((block): block is TextContent => block.type === \"text\")\n\t\t.map((block) => block.text)\n\t\t.join(\"\\n\");\n}\n\nfunction extractUserText(message: UserMessage): string {\n\tif (typeof message.content === \"string\") {\n\t\treturn message.content;\n\t}\n\treturn extractTextBlocks(message.content);\n}\n\nfunction extractAssistantText(message: AssistantMessage): string {\n\tconst parts: string[] = [];\n\n\t// Collect text blocks\n\tconst textParts = message.content\n\t\t.filter((block): block is TextContent => block.type === \"text\")\n\t\t.map((block) => block.text);\n\n\tif (textParts.length > 0) {\n\t\tparts.push(textParts.join(\"\\n\"));\n\t}\n\n\t// Collect thinking blocks with header\n\tfor (const block of message.content) {\n\t\tif (block.type === \"thinking\" && \"thinking\" in block) {\n\t\t\tparts.push(`[thinking]\\n${block.thinking}`);\n\t\t}\n\t}\n\n\treturn parts.join(\"\\n\");\n}\n\nfunction extractToolResultText(message: ToolResultMessage): string {\n\tconst textContent = extractTextBlocks(message.content);\n\tif (!textContent) {\n\t\treturn \"\";\n\t}\n\treturn `[${message.toolName}]\\n${textContent}`;\n}\n\nfunction extractBashText(message: BashExecutionMessage): string {\n\treturn `$ ${message.command}\\n${message.output}`;\n}\n\nfunction extractCustomText(message: CustomMessage): string {\n\tif (typeof message.content === \"string\") {\n\t\treturn message.content;\n\t}\n\tif (Array.isArray(message.content)) {\n\t\treturn extractTextBlocks(message.content);\n\t}\n\treturn \"\";\n}\n"]}
|
package/docs/keybindings.md
CHANGED
|
@@ -88,6 +88,7 @@ Modifier combinations: `ctrl+shift+x`, `alt+ctrl+x`, `ctrl+shift+alt+x`, `ctrl+1
|
|
|
88
88
|
| `app.suspend` | `ctrl+z` | Suspend to background |
|
|
89
89
|
| `app.editor.external` | `ctrl+g` | Open in external editor (`$VISUAL` or `$EDITOR`) |
|
|
90
90
|
| `app.clipboard.pasteImage` | `ctrl+v` (`alt+v` on Windows) | Paste image from clipboard |
|
|
91
|
+
| `app.clipboard.copyMessages` | `ctrl+shift+c` | Open copy message selector |
|
|
91
92
|
| `app.tasks.toggle` | *(none)* | Toggle task tracking panel visibility |
|
|
92
93
|
|
|
93
94
|
### Sessions
|