mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 22:35:51 +00:00
更新大量 tsx 原始文件; 已经迁移 login panel; 部分 (#121)
* style(B1-1): 格式化 ink/buddy/cli/context/screens/tasks/services/keybindings/state (43 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 修复了 Box.tsx 和 ScrollBox.tsx 中无效的 global.d.ts import。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-2): 格式化 commands (79 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-3): 格式化 components/messages,permissions,mcp,sandbox,shell (104 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-4): 格式化 components/PromptInput,FeedbackSurvey,tasks,agents,skills,design-system,wizard (73 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-5): 格式化 components其余 + hooks + tools (232 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-6): 格式化 main/entrypoints/utils/moreright (21 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: 更新 README,新增 Run.ps1/TODO.md,删除 V6.md - README.md: 大幅重写,更详细版本历史和配置示例 - Run.ps1: 新增 Windows 启动脚本 - TODO.md: 新增包完成清单 - V6.md: 删除(架构重构规划已不适用) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: 修复以前的问题 * fix: 修复 login 面板的问题 --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,45 +1,44 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { mkdir, writeFile } from 'fs/promises';
|
||||
import { marked, type Tokens } from 'marked';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import React, { useRef } from 'react';
|
||||
import type { CommandResultDisplay } from '../../commands.js';
|
||||
import type { OptionWithDescription } from '../../components/CustomSelect/select.js';
|
||||
import { Select } from '../../components/CustomSelect/select.js';
|
||||
import { Byline } from '../../components/design-system/Byline.js';
|
||||
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js';
|
||||
import { Pane } from '../../components/design-system/Pane.js';
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
||||
import { stringWidth } from '../../ink/stringWidth.js';
|
||||
import { setClipboard } from '../../ink/termio/osc.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { logEvent } from '../../services/analytics/index.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import type { AssistantMessage, Message } from '../../types/message.js';
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
|
||||
import { extractTextContent, stripPromptXMLTags } from '../../utils/messages.js';
|
||||
import { countCharInString } from '../../utils/stringUtils.js';
|
||||
const COPY_DIR = join(tmpdir(), 'claude');
|
||||
const RESPONSE_FILENAME = 'response.md';
|
||||
const MAX_LOOKBACK = 20;
|
||||
import { mkdir, writeFile } from 'fs/promises'
|
||||
import { marked, type Tokens } from 'marked'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
import React, { useRef } from 'react'
|
||||
import type { CommandResultDisplay } from '../../commands.js'
|
||||
import type { OptionWithDescription } from '../../components/CustomSelect/select.js'
|
||||
import { Select } from '../../components/CustomSelect/select.js'
|
||||
import { Byline } from '../../components/design-system/Byline.js'
|
||||
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js'
|
||||
import { Pane } from '../../components/design-system/Pane.js'
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||
import { stringWidth } from '../../ink/stringWidth.js'
|
||||
import { setClipboard } from '../../ink/termio/osc.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { logEvent } from '../../services/analytics/index.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
import type { AssistantMessage, Message } from '../../types/message.js'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
|
||||
import { extractTextContent, stripPromptXMLTags } from '../../utils/messages.js'
|
||||
import { countCharInString } from '../../utils/stringUtils.js'
|
||||
|
||||
const COPY_DIR = join(tmpdir(), 'claude')
|
||||
const RESPONSE_FILENAME = 'response.md'
|
||||
const MAX_LOOKBACK = 20
|
||||
|
||||
type CodeBlock = {
|
||||
code: string;
|
||||
lang: string | undefined;
|
||||
};
|
||||
code: string
|
||||
lang: string | undefined
|
||||
}
|
||||
|
||||
function extractCodeBlocks(markdown: string): CodeBlock[] {
|
||||
const tokens = marked.lexer(stripPromptXMLTags(markdown));
|
||||
const blocks: CodeBlock[] = [];
|
||||
const tokens = marked.lexer(stripPromptXMLTags(markdown))
|
||||
const blocks: CodeBlock[] = []
|
||||
for (const token of tokens) {
|
||||
if (token.type === 'code') {
|
||||
const codeToken = token as Tokens.Code;
|
||||
blocks.push({
|
||||
code: codeToken.text,
|
||||
lang: codeToken.lang
|
||||
});
|
||||
const codeToken = token as Tokens.Code
|
||||
blocks.push({ code: codeToken.text, lang: codeToken.lang })
|
||||
}
|
||||
}
|
||||
return blocks;
|
||||
return blocks
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,323 +47,267 @@ function extractCodeBlocks(markdown: string): CodeBlock[] {
|
||||
* Index 0 = latest, 1 = second-to-latest, etc. Caps at MAX_LOOKBACK.
|
||||
*/
|
||||
export function collectRecentAssistantTexts(messages: Message[]): string[] {
|
||||
const texts: string[] = [];
|
||||
for (let i = messages.length - 1; i >= 0 && texts.length < MAX_LOOKBACK; i--) {
|
||||
const msg = messages[i];
|
||||
if (msg?.type !== 'assistant' || msg.isApiErrorMessage) continue;
|
||||
const content = (msg as AssistantMessage).message.content;
|
||||
if (!Array.isArray(content)) continue;
|
||||
const text = extractTextContent(content, '\n\n');
|
||||
if (text) texts.push(text);
|
||||
const texts: string[] = []
|
||||
for (
|
||||
let i = messages.length - 1;
|
||||
i >= 0 && texts.length < MAX_LOOKBACK;
|
||||
i--
|
||||
) {
|
||||
const msg = messages[i]
|
||||
if (msg?.type !== 'assistant' || msg.isApiErrorMessage) continue
|
||||
const content = (msg as AssistantMessage).message.content
|
||||
if (!Array.isArray(content)) continue
|
||||
const text = extractTextContent(content, '\n\n')
|
||||
if (text) texts.push(text)
|
||||
}
|
||||
return texts;
|
||||
return texts
|
||||
}
|
||||
|
||||
export function fileExtension(lang: string | undefined): string {
|
||||
if (lang) {
|
||||
// Sanitize to prevent path traversal (e.g. ```../../etc/passwd)
|
||||
// Language identifiers are alphanumeric: python, tsx, jsonc, etc.
|
||||
const sanitized = lang.replace(/[^a-zA-Z0-9]/g, '');
|
||||
const sanitized = lang.replace(/[^a-zA-Z0-9]/g, '')
|
||||
if (sanitized && sanitized !== 'plaintext') {
|
||||
return `.${sanitized}`;
|
||||
return `.${sanitized}`
|
||||
}
|
||||
}
|
||||
return '.txt';
|
||||
return '.txt'
|
||||
}
|
||||
|
||||
async function writeToFile(text: string, filename: string): Promise<string> {
|
||||
const filePath = join(COPY_DIR, filename);
|
||||
await mkdir(COPY_DIR, {
|
||||
recursive: true
|
||||
});
|
||||
await writeFile(filePath, text, 'utf-8');
|
||||
return filePath;
|
||||
const filePath = join(COPY_DIR, filename)
|
||||
await mkdir(COPY_DIR, { recursive: true })
|
||||
await writeFile(filePath, text, 'utf-8')
|
||||
return filePath
|
||||
}
|
||||
async function copyOrWriteToFile(text: string, filename: string): Promise<string> {
|
||||
const raw = await setClipboard(text);
|
||||
if (raw) process.stdout.write(raw);
|
||||
const lineCount = countCharInString(text, '\n') + 1;
|
||||
const charCount = text.length;
|
||||
|
||||
async function copyOrWriteToFile(
|
||||
text: string,
|
||||
filename: string,
|
||||
): Promise<string> {
|
||||
const raw = await setClipboard(text)
|
||||
if (raw) process.stdout.write(raw)
|
||||
const lineCount = countCharInString(text, '\n') + 1
|
||||
const charCount = text.length
|
||||
// Also write to a temp file — clipboard paths are best-effort (OSC 52 needs
|
||||
// terminal support), so the file provides a reliable fallback.
|
||||
try {
|
||||
const filePath = await writeToFile(text, filename);
|
||||
return `Copied to clipboard (${charCount} characters, ${lineCount} lines)\nAlso written to ${filePath}`;
|
||||
const filePath = await writeToFile(text, filename)
|
||||
return `Copied to clipboard (${charCount} characters, ${lineCount} lines)\nAlso written to ${filePath}`
|
||||
} catch {
|
||||
return `Copied to clipboard (${charCount} characters, ${lineCount} lines)`;
|
||||
return `Copied to clipboard (${charCount} characters, ${lineCount} lines)`
|
||||
}
|
||||
}
|
||||
|
||||
function truncateLine(text: string, maxLen: number): string {
|
||||
const firstLine = text.split('\n')[0] ?? '';
|
||||
const firstLine = text.split('\n')[0] ?? ''
|
||||
if (stringWidth(firstLine) <= maxLen) {
|
||||
return firstLine;
|
||||
return firstLine
|
||||
}
|
||||
let result = '';
|
||||
let width = 0;
|
||||
const targetWidth = maxLen - 1;
|
||||
let result = ''
|
||||
let width = 0
|
||||
const targetWidth = maxLen - 1
|
||||
for (const char of firstLine) {
|
||||
const charWidth = stringWidth(char);
|
||||
if (width + charWidth > targetWidth) break;
|
||||
result += char;
|
||||
width += charWidth;
|
||||
const charWidth = stringWidth(char)
|
||||
if (width + charWidth > targetWidth) break
|
||||
result += char
|
||||
width += charWidth
|
||||
}
|
||||
return result + '\u2026';
|
||||
return result + '\u2026'
|
||||
}
|
||||
|
||||
type PickerProps = {
|
||||
fullText: string;
|
||||
codeBlocks: CodeBlock[];
|
||||
messageAge: number;
|
||||
onDone: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
};
|
||||
type PickerSelection = number | 'full' | 'always';
|
||||
function CopyPicker(t0) {
|
||||
const $ = _c(33);
|
||||
const {
|
||||
fullText,
|
||||
codeBlocks,
|
||||
messageAge,
|
||||
onDone
|
||||
} = t0;
|
||||
const focusedRef = useRef("full");
|
||||
const t1 = `${fullText.length} chars, ${countCharInString(fullText, "\n") + 1} lines`;
|
||||
let t2;
|
||||
if ($[0] !== t1) {
|
||||
t2 = {
|
||||
label: "Full response",
|
||||
value: "full" as const,
|
||||
description: t1
|
||||
};
|
||||
$[0] = t1;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
let t3;
|
||||
if ($[2] !== codeBlocks || $[3] !== t2) {
|
||||
let t4;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = {
|
||||
label: "Always copy full response",
|
||||
value: "always" as const,
|
||||
description: "Skip this picker in the future (revert via /config)"
|
||||
};
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
t3 = [t2, ...codeBlocks.map(_temp), t4];
|
||||
$[2] = codeBlocks;
|
||||
$[3] = t2;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
const options = t3;
|
||||
let t4;
|
||||
if ($[6] !== codeBlocks || $[7] !== fullText) {
|
||||
t4 = function getSelectionContent(selected) {
|
||||
if (selected === "full" || selected === "always") {
|
||||
return {
|
||||
text: fullText,
|
||||
filename: RESPONSE_FILENAME
|
||||
};
|
||||
}
|
||||
const block_0 = codeBlocks[selected];
|
||||
fullText: string
|
||||
codeBlocks: CodeBlock[]
|
||||
messageAge: number
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
}
|
||||
|
||||
type PickerSelection = number | 'full' | 'always'
|
||||
|
||||
function CopyPicker({
|
||||
fullText,
|
||||
codeBlocks,
|
||||
messageAge,
|
||||
onDone,
|
||||
}: PickerProps): React.ReactNode {
|
||||
const focusedRef = useRef<PickerSelection>('full')
|
||||
|
||||
const options: OptionWithDescription<PickerSelection>[] = [
|
||||
{
|
||||
label: 'Full response',
|
||||
value: 'full' as const,
|
||||
description: `${fullText.length} chars, ${countCharInString(fullText, '\n') + 1} lines`,
|
||||
},
|
||||
...codeBlocks.map((block, index) => {
|
||||
const blockLines = countCharInString(block.code, '\n') + 1
|
||||
return {
|
||||
text: block_0.code,
|
||||
filename: `copy${fileExtension(block_0.lang)}`,
|
||||
blockIndex: selected
|
||||
};
|
||||
};
|
||||
$[6] = codeBlocks;
|
||||
$[7] = fullText;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
const getSelectionContent = t4;
|
||||
let t5;
|
||||
if ($[9] !== codeBlocks.length || $[10] !== getSelectionContent || $[11] !== messageAge || $[12] !== onDone) {
|
||||
t5 = async function handleSelect(selected_0) {
|
||||
const content = getSelectionContent(selected_0);
|
||||
if (selected_0 === "always") {
|
||||
if (!getGlobalConfig().copyFullResponse) {
|
||||
saveGlobalConfig(_temp2);
|
||||
}
|
||||
logEvent("tengu_copy", {
|
||||
block_count: codeBlocks.length,
|
||||
always: true,
|
||||
message_age: messageAge
|
||||
});
|
||||
const result = await copyOrWriteToFile(content.text, content.filename);
|
||||
onDone(`${result}\nPreference saved. Use /config to change copyFullResponse`);
|
||||
return;
|
||||
label: truncateLine(block.code, 60),
|
||||
value: index,
|
||||
description:
|
||||
[block.lang, blockLines > 1 ? `${blockLines} lines` : undefined]
|
||||
.filter(Boolean)
|
||||
.join(', ') || undefined,
|
||||
}
|
||||
logEvent("tengu_copy", {
|
||||
selected_block: content.blockIndex,
|
||||
block_count: codeBlocks.length,
|
||||
message_age: messageAge
|
||||
});
|
||||
const result_0 = await copyOrWriteToFile(content.text, content.filename);
|
||||
onDone(result_0);
|
||||
};
|
||||
$[9] = codeBlocks.length;
|
||||
$[10] = getSelectionContent;
|
||||
$[11] = messageAge;
|
||||
$[12] = onDone;
|
||||
$[13] = t5;
|
||||
} else {
|
||||
t5 = $[13];
|
||||
}),
|
||||
{
|
||||
label: 'Always copy full response',
|
||||
value: 'always' as const,
|
||||
description: 'Skip this picker in the future (revert via /config)',
|
||||
},
|
||||
]
|
||||
|
||||
function getSelectionContent(selected: PickerSelection): {
|
||||
text: string
|
||||
filename: string
|
||||
blockIndex?: number
|
||||
} {
|
||||
if (selected === 'full' || selected === 'always') {
|
||||
return { text: fullText, filename: RESPONSE_FILENAME }
|
||||
}
|
||||
const block = codeBlocks[selected]!
|
||||
return {
|
||||
text: block.code,
|
||||
filename: `copy${fileExtension(block.lang)}`,
|
||||
blockIndex: selected,
|
||||
}
|
||||
}
|
||||
const handleSelect = t5;
|
||||
let t6;
|
||||
if ($[14] !== codeBlocks.length || $[15] !== getSelectionContent || $[16] !== messageAge || $[17] !== onDone) {
|
||||
const handleWrite = async function handleWrite(selected_1) {
|
||||
const content_0 = getSelectionContent(selected_1);
|
||||
logEvent("tengu_copy", {
|
||||
selected_block: content_0.blockIndex,
|
||||
|
||||
async function handleSelect(selected: PickerSelection): Promise<void> {
|
||||
const content = getSelectionContent(selected)
|
||||
if (selected === 'always') {
|
||||
if (!getGlobalConfig().copyFullResponse) {
|
||||
saveGlobalConfig(c => ({ ...c, copyFullResponse: true }))
|
||||
}
|
||||
logEvent('tengu_copy', {
|
||||
block_count: codeBlocks.length,
|
||||
always: true,
|
||||
message_age: messageAge,
|
||||
write_shortcut: true
|
||||
});
|
||||
;
|
||||
try {
|
||||
const filePath = await writeToFile(content_0.text, content_0.filename);
|
||||
onDone(`Written to ${filePath}`);
|
||||
} catch (t7) {
|
||||
const e = t7;
|
||||
onDone(`Failed to write file: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
};
|
||||
t6 = function handleKeyDown(e_0) {
|
||||
if (e_0.key === "w") {
|
||||
e_0.preventDefault();
|
||||
handleWrite(focusedRef.current);
|
||||
}
|
||||
};
|
||||
$[14] = codeBlocks.length;
|
||||
$[15] = getSelectionContent;
|
||||
$[16] = messageAge;
|
||||
$[17] = onDone;
|
||||
$[18] = t6;
|
||||
} else {
|
||||
t6 = $[18];
|
||||
})
|
||||
const result = await copyOrWriteToFile(content.text, content.filename)
|
||||
onDone(
|
||||
`${result}\nPreference saved. Use /config to change copyFullResponse`,
|
||||
)
|
||||
return
|
||||
}
|
||||
logEvent('tengu_copy', {
|
||||
selected_block: content.blockIndex,
|
||||
block_count: codeBlocks.length,
|
||||
message_age: messageAge,
|
||||
})
|
||||
const result = await copyOrWriteToFile(content.text, content.filename)
|
||||
onDone(result)
|
||||
}
|
||||
const handleKeyDown = t6;
|
||||
let t7;
|
||||
if ($[19] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = <Text dimColor={true}>Select content to copy:</Text>;
|
||||
$[19] = t7;
|
||||
} else {
|
||||
t7 = $[19];
|
||||
|
||||
async function handleWrite(selected: PickerSelection): Promise<void> {
|
||||
const content = getSelectionContent(selected)
|
||||
logEvent('tengu_copy', {
|
||||
selected_block: content.blockIndex,
|
||||
block_count: codeBlocks.length,
|
||||
message_age: messageAge,
|
||||
write_shortcut: true,
|
||||
})
|
||||
try {
|
||||
const filePath = await writeToFile(content.text, content.filename)
|
||||
onDone(`Written to ${filePath}`)
|
||||
} catch (e) {
|
||||
onDone(`Failed to write file: ${e instanceof Error ? e.message : e}`)
|
||||
}
|
||||
}
|
||||
let t8;
|
||||
if ($[20] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = value => {
|
||||
focusedRef.current = value;
|
||||
};
|
||||
$[20] = t8;
|
||||
} else {
|
||||
t8 = $[20];
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent): void {
|
||||
if (e.key === 'w') {
|
||||
e.preventDefault()
|
||||
void handleWrite(focusedRef.current)
|
||||
}
|
||||
}
|
||||
let t9;
|
||||
if ($[21] !== handleSelect) {
|
||||
t9 = selected_2 => {
|
||||
handleSelect(selected_2);
|
||||
};
|
||||
$[21] = handleSelect;
|
||||
$[22] = t9;
|
||||
} else {
|
||||
t9 = $[22];
|
||||
}
|
||||
let t10;
|
||||
if ($[23] !== onDone) {
|
||||
t10 = () => {
|
||||
onDone("Copy cancelled", {
|
||||
display: "system"
|
||||
});
|
||||
};
|
||||
$[23] = onDone;
|
||||
$[24] = t10;
|
||||
} else {
|
||||
t10 = $[24];
|
||||
}
|
||||
let t11;
|
||||
if ($[25] !== options || $[26] !== t10 || $[27] !== t9) {
|
||||
t11 = <Select options={options} hideIndexes={false} onFocus={t8} onChange={t9} onCancel={t10} />;
|
||||
$[25] = options;
|
||||
$[26] = t10;
|
||||
$[27] = t9;
|
||||
$[28] = t11;
|
||||
} else {
|
||||
t11 = $[28];
|
||||
}
|
||||
let t12;
|
||||
if ($[29] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t12 = <Text dimColor={true}><Byline><KeyboardShortcutHint shortcut="enter" action="copy" /><KeyboardShortcutHint shortcut="w" action="write to file" /><KeyboardShortcutHint shortcut="esc" action="cancel" /></Byline></Text>;
|
||||
$[29] = t12;
|
||||
} else {
|
||||
t12 = $[29];
|
||||
}
|
||||
let t13;
|
||||
if ($[30] !== handleKeyDown || $[31] !== t11) {
|
||||
t13 = <Pane><Box flexDirection="column" gap={1} tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t7}{t11}{t12}</Box></Pane>;
|
||||
$[30] = handleKeyDown;
|
||||
$[31] = t11;
|
||||
$[32] = t13;
|
||||
} else {
|
||||
t13 = $[32];
|
||||
}
|
||||
return t13;
|
||||
}
|
||||
function _temp2(c) {
|
||||
return {
|
||||
...c,
|
||||
copyFullResponse: true
|
||||
};
|
||||
}
|
||||
function _temp(block, index) {
|
||||
const blockLines = countCharInString(block.code, "\n") + 1;
|
||||
return {
|
||||
label: truncateLine(block.code, 60),
|
||||
value: index,
|
||||
description: [block.lang, blockLines > 1 ? `${blockLines} lines` : undefined].filter(Boolean).join(", ") || undefined
|
||||
};
|
||||
|
||||
return (
|
||||
<Pane>
|
||||
<Box
|
||||
flexDirection="column"
|
||||
gap={1}
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Text dimColor>Select content to copy:</Text>
|
||||
<Select<PickerSelection>
|
||||
options={options}
|
||||
hideIndexes={false}
|
||||
onFocus={value => {
|
||||
focusedRef.current = value
|
||||
}}
|
||||
onChange={selected => {
|
||||
void handleSelect(selected)
|
||||
}}
|
||||
onCancel={() => {
|
||||
onDone('Copy cancelled', { display: 'system' })
|
||||
}}
|
||||
/>
|
||||
<Text dimColor>
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="enter" action="copy" />
|
||||
<KeyboardShortcutHint shortcut="w" action="write to file" />
|
||||
<KeyboardShortcutHint shortcut="esc" action="cancel" />
|
||||
</Byline>
|
||||
</Text>
|
||||
</Box>
|
||||
</Pane>
|
||||
)
|
||||
}
|
||||
|
||||
export const call: LocalJSXCommandCall = async (onDone, context, args) => {
|
||||
const texts = collectRecentAssistantTexts(context.messages);
|
||||
const texts = collectRecentAssistantTexts(context.messages)
|
||||
|
||||
if (texts.length === 0) {
|
||||
onDone('No assistant message to copy');
|
||||
return null;
|
||||
onDone('No assistant message to copy')
|
||||
return null
|
||||
}
|
||||
|
||||
// /copy N reaches back N-1 messages (1 = latest, 2 = second-to-latest, ...)
|
||||
let age = 0;
|
||||
const arg = args?.trim();
|
||||
let age = 0
|
||||
const arg = args?.trim()
|
||||
if (arg) {
|
||||
const n = Number(arg);
|
||||
const n = Number(arg)
|
||||
if (!Number.isInteger(n) || n < 1) {
|
||||
onDone(`Usage: /copy [N] where N is 1 (latest), 2, 3, \u2026 Got: ${arg}`);
|
||||
return null;
|
||||
onDone(`Usage: /copy [N] where N is 1 (latest), 2, 3, \u2026 Got: ${arg}`)
|
||||
return null
|
||||
}
|
||||
if (n > texts.length) {
|
||||
onDone(`Only ${texts.length} assistant ${texts.length === 1 ? 'message' : 'messages'} available to copy`);
|
||||
return null;
|
||||
onDone(
|
||||
`Only ${texts.length} assistant ${texts.length === 1 ? 'message' : 'messages'} available to copy`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
age = n - 1;
|
||||
age = n - 1
|
||||
}
|
||||
const text = texts[age]!;
|
||||
const codeBlocks = extractCodeBlocks(text);
|
||||
const config = getGlobalConfig();
|
||||
|
||||
const text = texts[age]!
|
||||
const codeBlocks = extractCodeBlocks(text)
|
||||
const config = getGlobalConfig()
|
||||
|
||||
if (codeBlocks.length === 0 || config.copyFullResponse) {
|
||||
logEvent('tengu_copy', {
|
||||
always: config.copyFullResponse,
|
||||
block_count: codeBlocks.length,
|
||||
message_age: age
|
||||
});
|
||||
const result = await copyOrWriteToFile(text, RESPONSE_FILENAME);
|
||||
onDone(result);
|
||||
return null;
|
||||
message_age: age,
|
||||
})
|
||||
const result = await copyOrWriteToFile(text, RESPONSE_FILENAME)
|
||||
onDone(result)
|
||||
return null
|
||||
}
|
||||
return <CopyPicker fullText={text} codeBlocks={codeBlocks} messageAge={age} onDone={onDone} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<CopyPicker
|
||||
fullText={text}
|
||||
codeBlocks={codeBlocks}
|
||||
messageAge={age}
|
||||
onDone={onDone}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user