From 09cd992aa6d27d88b5dec0ea739cc1c7de433a17 Mon Sep 17 00:00:00 2001 From: kim Date: Mon, 24 Aug 2026 12:44:08 +0900 Subject: [PATCH] feat: normal screen architecture, burn rate by model time, dependency upgrades MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major changes: - Replace alternate screen buffer + app-side mouse tracking/virtual scroll with normal screen buffer + Ink for permanent scrollback. Terminal's native scroll/selection/copy just works โ€” no mouseInput.ts needed. - Fix burn rate (๐Ÿ”ฅ) to use model response time (modelTimeMs) instead of session elapsed time, so it reflects actual generation throughput. - Upgrade dependencies: openai 6โ†’7, commander 13โ†’15, execa 9โ†’10, vitest 3โ†’4, node types 22โ†’26, tsup target node20โ†’node22. - Update upgrade memo with current architecture, all completed upgrades, and working tree status. --- locode-upgrade-memo.md | 740 ++++-------------- package-lock.json | 1084 ++++++++++++++++++++------ package.json | 24 +- src/agent/loop.test.ts | 70 +- src/agent/loop.ts | 62 +- src/codeintel/lspManager.ts | 28 +- src/config/defaults.ts | 17 +- src/config/store.ts | 22 +- src/persistence/sessionStore.test.ts | 15 +- src/persistence/sessionStore.ts | 107 ++- src/ui/ink/App.tsx | 319 +------- src/ui/ink/ChatInput.tsx | 7 - src/ui/ink/FilePanel.tsx | 8 +- src/ui/ink/HistoryItemView.tsx | 4 +- src/ui/ink/StatusBar.tsx | 12 +- src/ui/ink/index.tsx | 35 +- src/ui/ink/mouseInput.ts | 105 --- tsup.config.ts | 2 +- 18 files changed, 1321 insertions(+), 1340 deletions(-) delete mode 100644 src/ui/ink/mouseInput.ts diff --git a/locode-upgrade-memo.md b/locode-upgrade-memo.md index faf247f..f44f9ef 100644 --- a/locode-upgrade-memo.md +++ b/locode-upgrade-memo.md @@ -6,631 +6,181 @@ - ํ•ต์‹ฌ ์ฒ ํ•™: "์‹ ๋ขฐํ•  ์ˆ˜ ์—†๊ณ  ๋А๋ฆฌ๊ณ  ๋น„์ „/ํˆด์ฝœ ์ง€์›์ด ๋ถˆํ™•์‹คํ•œ ๋กœ์ปฌ ๋ชจ๋ธ"์ด๋ผ๋Š” ํ˜„์‹ค์— ๋งž์ถฐ ๋ชจ๋“  ๊ฐ€์ •์„ ๋น„๊ด€์ ์œผ๋กœ ์žฌ๋‹จ - Claude Code ํ”Œ๋Ÿฌ๊ทธ์ธ ํฌ๋งท์„ ์ง์ ‘ ์†Œ๋น„ํ•˜๋Š” ํ•˜์œ„ํ˜ธํ™˜ ๋ธŒ๋ฆฌ์ง€ (`.claude-plugin/plugin.json`, commands/agents/skills/hooks/MCP) -## ๋นŒ๋“œ/ํ…Œ์ŠคํŠธ ์ƒํƒœ -- `npm run build` (tsup) โ€” ๊นจ๋— +## ๋นŒ๋“œ/ํ…Œ์ŠคํŠธ ์ƒํƒœ (์ตœ์‹ : c539995) - `npx tsc --noEmit` โ€” ๊นจ๋— -- `npm test` (vitest) โ€” 29 ํŒŒ์ผ 215๊ฐœ ์ „๋ถ€ ํ†ต๊ณผ -- ํŒŒ์ผ ์ธ์ฝ”๋”ฉ: **CRLF** (edit_file ๋„๊ตฌ๊ฐ€ LF๋กœ ์ •๊ทœํ™”ํ•ด์„œ ๋งค์นญ ์‹คํŒจ โ†’ Python ์Šคํฌ๋ฆฝํŠธ๋กœ ๋ฐ”์ด๋„ˆ๋ฆฌ ํŽธ์ง‘ํ•ด์•ผ ํ•จ) +- `npm run build` (tsup) โ€” 290.75 KB +- `npm test` (vitest) โ€” **43 ํŒŒ์ผ 347๊ฐœ ์ „๋ถ€ ํ†ต๊ณผ** +- Node ํƒ€๊ฒŸ: **node22** (tsup.config.ts์—์„œ ๋ณ€๊ฒฝ) +- ํŒŒ์ผ ์ธ์ฝ”๋”ฉ: CRLF/LF ์„น์…˜ ํ˜ผ์žฌ (edit_file์˜ EOL ์ •๊ทœํ™” ๋กœ์ง์œผ๋กœ ์ฒ˜๋ฆฌ) --- -## ๋ฐœ๊ฒฌํ•œ ์—…๊ทธ๋ ˆ์ด๋“œ ํ›„๋ณด (12๊ฐœ) +## ์•„ํ‚คํ…์ฒ˜ ๊ฐœ์š” (ํ˜„์žฌ) -| # | ํ•ญ๋ชฉ | ๋‚œ์ด๋„ | ํšจ๊ณผ | ๋กœ์ปฌ ํŠนํ™” | ์ƒํƒœ | -|---|---|:---:|:---:|:---:|:---:| -| 1 | ๋ณ‘๋ ฌ ํˆด ์‹คํ–‰ (read-only) | ์ค‘ | ๋Œ€ | โ˜… | **์™„๋ฃŒ** โœ… | -| 2 | ์žฌ์‹œ๋„ ์ •์ฑ… ์„ค์ •ํ™” (`maxRetries`) | ํ•˜ | ์ค‘ | โ˜… | **์™„๋ฃŒ** โœ… | -| 3 | ์ •ํ™•ํ•œ ํ† ํฐ ์ถ”์ • (`/api/tokenize` ๋˜๋Š” BPE) | ์ค‘ | ๋Œ€ | โ˜…โ˜… | **์™„๋ฃŒ** โœ… | -| 4 | ์Šค๋งˆํŠธ ์ถœ๋ ฅ ์บก (head+tail, ๋ผ์ธ ๊ธธ์ด) | ํ•˜ | ์ค‘ | โ˜… | **์™„๋ฃŒ** โœ… | -| 5 | ๋ถ€๋ถ„ ํžˆ์Šคํ† ๋ฆฌ ๋ณด์กด ์ปดํŒฉ์…˜ | **์ƒ** | ๋Œ€ | โ˜… | **์™„๋ฃŒ** โœ… | -| 6 | ๋™์  `max_tokens` | ํ•˜ | ๋Œ€ | โ˜… | **์™„๋ฃŒ** โœ… | -| 7 | ํˆด ์„ค๋ช… ํ’๋ถ€ํ™” + ๋™์  ํˆด ์„ ํƒ | ์ค‘ | ์ค‘ | โ˜… | **์™„๋ฃŒ** โœ… | -| 8 | MCP ์—ฐ๊ฒฐ ์žฌ์‹œ๋„ยท์žฌ์—ฐ๊ฒฐ | ์ค‘ | ์ค‘ | | **์™„๋ฃŒ** โœ… | -| 9 | ์ปจํ…์ŠคํŠธ ์œˆ๋„์šฐ ์บ์‹œ TTL | ํ•˜ | ์ค‘ | โ˜… | **์™„๋ฃŒ** โœ… | -| 10 | `edit_file` ์œ ์‚ฌ ๋งค์น˜ ์ œ์•ˆ | ์ค‘ | ๋Œ€ | โ˜…โ˜… | **์™„๋ฃŒ** โœ… | -| 11 | git_status ์ถœ๋ ฅ head+tail | ํ•˜ | ์ค‘ | | **์™„๋ฃŒ** โœ… | -| 12 | `auto-accept` ๋ชจ๋“œ ์„ธ๋ถ„ํ™” | ์ค‘ | ์ค‘ | | **์™„๋ฃŒ** โœ… | +### ๋ Œ๋”๋ง ๋ชจ๋ธ: Normal Screen + `` +**๋Œ€ํญ ๋ณ€๊ฒฝ**: ์ด์ „ alternate screen buffer + ์ธ์•ฑ ๊ฐ€์ƒ ์Šคํฌ๋กค + ๋งˆ์šฐ์Šค ํŠธ๋ž˜ํ‚น ์•„ํ‚คํ…์ฒ˜๋ฅผ ์™„์ „ํžˆ ํ๊ธฐ. + +- **Normal screen buffer**: `index.tsx`๊ฐ€ alternate screen์— ์ง„์ž…ํ•˜์ง€ ์•Š์Œ. ์•ฑ์ด ํ„ฐ๋ฏธ๋„์˜ ์ผ๋ฐ˜ ์Šคํฌ๋กค๋ฐฑ์— ์ง์ ‘ ์ถœ๋ ฅ. +- **Ink ``**: ์™„๋ฃŒ๋œ ํ„ด/์ด๋ฒคํŠธ๋ฅผ `` ์ปดํฌ๋„ŒํŠธ๋กœ ํ•œ ๋ฒˆ๋งŒ ๋ Œ๋”๋ง โ†’ ํ„ฐ๋ฏธ๋„ ์Šคํฌ๋กค๋ฐฑ์˜ ์˜๊ตฌ ๋ถ€๋ถ„์ด ๋จ. ์žฌ๋ Œ๋”๋ง ์—†์Œ. +- **๋งˆ์šฐ์Šค ํŠธ๋ž˜ํ‚น ์™„์ „ ์ œ๊ฑฐ**: `mouseInput.ts` ํŒŒ์ผ ์‚ญ์ œ. SGR-1006, `logicalButton()`, `copyToClipboard()`, `mouseToContentRow` ๋ชจ๋‘ ์ œ๊ฑฐ. ํ„ฐ๋ฏธ๋„ ์ž์ฒด ๋„ค์ดํ‹ฐ๋ธŒ ์Šคํฌ๋กค/์„ ํƒ/๋ณต์‚ฌ์— ์˜์กด. +- **์ด์ **: ์ฝ”๋“œ ๋Œ€ํญ ๊ฐ์†Œ, ํ„ฐ๋ฏธ๋„ ํ˜ธํ™˜์„ฑ ํ–ฅ์ƒ, SSH/tmux์—์„œ๋„ ๊ธฐ๋ณธ ์Šคํฌ๋กค ์ž‘๋™, ์ธ์•ฑ ๋งˆ์šฐ์Šค ๋ฒ„๊ทธ ๋ถˆ๊ฐ€. + +### ํ•ต์‹ฌ ํŒŒ์ผ ๋งต +- `src/ui/ink/index.tsx` โ€” ์ง„์ž…์ . alternate screen ์—†์ด Ink render. cleanup ์‹œ flush + ์ข…๋ฃŒ. +- `src/ui/ink/App.tsx` (1107ํ–‰) โ€” ๋ฉ”์ธ UI ์ปดํฌ๋„ŒํŠธ. `` + ๋ผ์ด๋ธŒ ์˜์—ญ. ์ƒํƒœ: startingโ†’connectingโ†’loading-modelsโ†’model-select/session-selectโ†’input. +- `src/ui/ink/ChatInput.tsx` โ€” ์ปค์Šคํ…€ multiline ์ž…๋ ฅ. Shift+Enter ์ค„๋ฐ”๊ฟˆ, bracket paste, @๋ฉ˜์…˜ fuzzy picker, IME ์ปค์„œ. +- `src/ui/ink/HistoryItemView.tsx` โ€” memo() ๋ž˜ํ•‘. thinking/streaming_text/assistant/tool_call ๋“ฑ ๋‹ค์–‘ํ•œ ์•„์ดํ…œ ๋ Œ๋”. +- `src/ui/ink/FilePanel.tsx` โ€” Ctrl+F ํ† ๊ธ€ ์‚ฌ์ด๋“œ ํŒจ๋„ (Files/Activity ํƒญ). +- `src/agent/loop.ts` (1284ํ–‰) โ€” ๋ฉ”์ธ ์—์ด์ „ํŠธ ๋ฃจํ”„. ํ„ด/์ŠคํŠธ๋ฆฌ๋ฐ/ํˆด์ฝœ/์ปดํŒฉ์…˜/์„œ๋ธŒ์—์ด์ „ํŠธ/๋ณ‘๋ ฌ ํˆด ๋ฐฐ์น˜/๋ฐ˜๋ณต ๋ฃจํ”„ ๊ฐ์ง€. +- `src/agent/session.ts` โ€” Session ๊ฐ์ฒด, ํ†ต๊ณ„, ์ƒํƒœ, mutation gate. +- `src/agent/systemPrompt.ts` โ€” ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ ๋นŒ๋” (๋กœ์ปฌ/ํด๋ผ์šฐ๋“œ ๋ถ„๊ธฐ). +- `src/agent/events.ts` โ€” AgentEvent ํƒ€์ž… (thinking_delta/thinking_done ์ถ”๊ฐ€). +- `src/tools/` โ€” 14+ ๋‚ด์žฅ ํˆด (read_file, list_files, grep, definition, references, diagnostics, web_search, web_fetch, git_status, write_file, edit_file, multi_edit, notebook_edit, bash, bash_output, bash_kill, git_commit, todo_write, task_create/list/get/update, agent). +- `src/toolcalling/` โ€” native ์–ด๋Œ‘ํ„ฐ, fallback ํŒŒ์„œ/ํ”„๋กฌํ”„ํŠธ, partialJson ๋ณต๊ตฌ, resolve (Ollama ๋นˆํ‚ค ๋ณต๊ตฌ ํฌํ•จ). +- `src/mcp/` โ€” MCP ํด๋ผ์ด์–ธํŠธ/๋งค๋‹ˆ์ €/์–ด๋Œ‘ํ„ฐ/config (๋ชจ๋“  MCP ํˆด mutating ๊ฐ•์ œ). +- `src/codeintel/lspManager.ts` (452ํ–‰) โ€” ์–ธ์–ด๋ณ„ LSP ์„œ๋ฒ„ lazy spawn (tsserver/pyright/gopls/rust-analyzer/clangd + ์‚ฌ์šฉ์ž ์„ค์ • ๊ฐ€๋Šฅ). +- `src/config/defaults.ts` โ€” ๋ชจ๋“  ๊ธฐ๋ณธ๊ฐ’. `DEFAULT_MAX_ITERATIONS = 300`, `DEFAULT_MAX_OUTPUT_TOKENS = 131072` ๋“ฑ. +- `src/config/store.ts` โ€” StoredConfig ํƒ€์ž…. ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ + ์˜์† ์„ค์ •. +- `src/persistence/sessionStore.ts` โ€” ์„ธ์…˜ CRUD, ์ธ๋ฑ์Šค ์ž๊ฐ€ ๋ณต๊ตฌ, ์›์ž ์“ฐ๊ธฐ, ํ์ž‰. +- `src/utils/` โ€” tokens(์Šคํฌ๋ฆฝํŠธ ์ธ์‹ ํœด๋ฆฌ์Šคํ‹ฑ), truncate(head+tail), diff, shell, processTree, image, html, mentions, projectInstructions, writeFileAtomic. --- -## โœ… ์™„๋ฃŒ: #6 ๋™์  `max_tokens` +## ์™„๋ฃŒ๋œ ์—…๊ทธ๋ ˆ์ด๋“œ ์ „์ฒด ๋ชฉ๋ก -### ๋ฌธ์ œ -`src/agent/loop.ts`์˜ ๋‘ ์ƒ์„ฑ ์š”์ฒญ์ด `max_tokens: 4096` ํ•˜๋“œ์ฝ”๋”ฉ: -- 729ํ–‰: ์ŠคํŠธ๋ฆฌ๋ฐ ์š”์ฒญ (`session.client.chat.completions.create`, `stream: true`) -- 840ํ–‰(โ†’์ด์ œ 856ํ–‰): ๋น„์ŠคํŠธ๋ฆฌ๋ฐ ์žฌ์‹œ๋„ (native ํˆด์ฝœ ์ธ์ž๊ฐ€ ๊นจ์กŒ์„ ๋•Œ) +### ์ดˆ๊ธฐ 12๊ฐœ (v0.6.0, 6fe9888) +| # | ํ•ญ๋ชฉ | ์ƒํƒœ | +|---|---|:---:| +| 1 | ๋ณ‘๋ ฌ ํˆด ์‹คํ–‰ (read-only Promise.all) | โœ… | +| 2 | ์žฌ์‹œ๋„ ์ •์ฑ… ์„ค์ •ํ™” (maxRetries) | โœ… | +| 3 | ์ •ํ™•ํ•œ ํ† ํฐ ์ถ”์ • (CJK/๊ธฐํ˜ธ ์Šคํฌ๋ฆฝํŠธ ์ธ์‹) | โœ… | +| 4 | ์Šค๋งˆํŠธ ์ถœ๋ ฅ ์บก (head+tail ๋ณด์กด) | โœ… | +| 5 | ๋ถ€๋ถ„ ํžˆ์Šคํ† ๋ฆฌ ๋ณด์กด ์ปดํŒฉ์…˜ | โœ… | +| 6 | ๋™์  max_tokens (resolveMaxTokens) | โœ… | +| 7 | ํˆด ์„ค๋ช… ํ’๋ถ€ํ™” | โœ… | +| 8 | MCP ์—ฐ๊ฒฐ ์žฌ์‹œ๋„ยท์žฌ์—ฐ๊ฒฐ | โœ… | +| 9 | ์ปจํ…์ŠคํŠธ ์œˆ๋„์šฐ ์บ์‹œ TTL | โœ… | +| 10 | edit_file ์œ ์‚ฌ ๋งค์น˜ ์ œ์•ˆ (Levenshtein) | โœ… | +| 11 | git ์ถœ๋ ฅ head+tail | โœ… | +| 12 | auto-accept ๋ชจ๋“œ ์„ธ๋ถ„ํ™” | โœ… | -๋กœ์ปฌ ๋ชจ๋ธ์ด ํŒŒ์ผ์„ ํ†ต์งธ๋กœ ๋‹ค์‹œ ์“ธ ๋•Œ(fallback ๋ชจ๋“œ์—์„œ ์ •๋ฐ€ edit์ด ์–ด๋ ค์›Œ ํ”ํ•จ) 4096 ํ† ํฐ์œผ๋กœ ๋ถ€์กฑ โ†’ ์‘๋‹ต ์ค‘๊ฐ„ ์ž˜๋ฆผ โ†’ ํˆด์ฝœ JSON ๋ถˆ์™„์ „ โ†’ malformed ์—๋Ÿฌ ๋ฐ˜๋ณต. ์ด๊ฒŒ "์ž๊พธ ์—๋Ÿฌ๊ฐ€ ๋‚˜"๋˜ ์›์ธ. +### 08-20 ์„ธ์…˜ (6fe9888..dfaf8d1) +- LSP ์ฝ”๋“œ ์ธํ…”๋ฆฌ์ „์Šค + ๋ณ‘๋ ฌ ์„œ๋ธŒ์—์ด์ „ํŠธ mutation gate +- ๋กœ์ปฌ ๋ชจ๋ธ ํˆด์ฝœ ์•ˆ์ •์„ฑ 3์ข… (๋ถ€๋ถ„ JSON ๋ณต๊ตฌ, fallback ํŒŒ์„œ, ๋นˆ ์‘๋‹ต ์žฌ์‹œ๋„) +- multi_edit, DiffView, notebook_edit, task ์‹œ์Šคํ…œ, ํŠธ๋žœ์Šคํฌ๋ฆฝํŠธ ๋‚ด๋ณด๋‚ด๊ธฐ +- /mouse ํ† ๊ธ€, ์ถœ๋ ฅ ์ค‘ ์ž…๋ ฅ์ฐฝ ์ž‘๋™ -### ํ•ด๊ฒฐ -`shouldAutoCompact` ๋’ค(157ํ–‰ ๊ทผ์ฒ˜)์— `resolveMaxTokens(session)` ํ—ฌํผ ์ถ”๊ฐ€: +### 08-21 ์„ธ์…˜ (dfaf8d1..5c3fcfd) +- ๋งˆ์šฐ์Šค ๊ธฐ๋ณธ ON + SGR-1006 ์ „์ฒด ํŒŒ์‹ฑ + ๋“œ๋ž˜๊ทธ ์„ ํƒ/OSC 52 ํด๋ฆฝ๋ณด๋“œ +- ๋ณ‘๋ ฌ ํˆด ์‹คํ–‰ ๊ฒ€์ฆ (runToolBatch ๋””๋ฒ„๊ทธ ๋กœ๊ทธ + ํ…Œ์ŠคํŠธ) +- maxIterations 100โ†’300 ์ƒํ–ฅ +- gateAndRun ์—๋Ÿฌ ๋กœ๊น… +- DiffView ์‚ญ์ œ ๋ผ์ธ ์ƒ‰์ƒ ์กฐ์ • (#f85149 โ†’ #e8904e) -```ts -function resolveMaxTokens(session: Session): number { - const MARGIN = 512; - const MIN = 2048; - const available = session.contextWindow - session.lastContextTokens - MARGIN; - return Math.max(MIN, Math.min(available, session.contextWindow)); -} -``` +### 08-22 ์„ธ์…˜ #1 (5c3fcfd..5065990): 15๊ฐœ ์—…๊ทธ๋ ˆ์ด๋“œ +| # | ํ•ญ๋ชฉ | ์ƒํƒœ | +|---|---|:---:| +| 1 | ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ ๋Œ€ํญ ๊ฐ•ํ™” (๋กœ์ปฌ/ํด๋ผ์šฐ๋“œ ๋ถ„๊ธฐ ํฌํ•จ) | โœ… | +| 2 | ๋ฉ€ํ‹ฐ๋ผ์ธ ์ž…๋ ฅ (Shift+Enter) + ๋ธŒ๋ž˜ํ‚ท ํŽ˜์ด์ŠคํŠธ | โœ… | +| 3 | /undo ๋ช…๋ น โ€” ๋งˆ์ง€๋ง‰ ํ„ด ๋กค๋ฐฑ | โœ… | +| 4 | /dashboard โ€” ์ด๋ฏธ ๊ตฌํ˜„๋จ | โœ… | +| 5 | ์ปจํ…์ŠคํŠธ ์‚ฌ์šฉ๋ฅ  ์ง„ํ–‰ ๋ฐ” โ€” ์ด๋ฏธ ๊ตฌํ˜„๋จ | โœ… | +| 6 | ์ถ”๋ก (thinking) ํ† ํฐ ์ง€์› | โœ… | +| 7 | @ ๋ฉ˜์…˜ ํผ์ง€ ๋งค์นญ + ์บ์‹œ TTL | โœ… | +| 8 | readFile ๋ฐ”์ด๋„ˆ๋ฆฌ ๊ฐ€๋“œ + ๋Œ€์šฉ๋Ÿ‰ ๋ณดํ˜ธ | โœ… | +| 9 | writeFile ์›์ž ์“ฐ๊ธฐ (tempโ†’rename) | โœ… | +| 10 | bash ๋ฌธ์ž์—ด ๋ˆ„์  O(nยฒ)โ†’O(n) | โœ… | +| 11 | estimateTokens ์ •๊ทœ์‹ ๊ณ ์†ํ™” | โœ… | +| 12 | handleCompletedMessage anyโ†’Record | โœ… | +| 13 | ์„ธ์…˜ ๋ณต์› ์‹œ ๊ถŒํ•œ ์ƒํƒœ ๋ณด์กด | โœ… | +| 14 | ์ปจํ…์ŠคํŠธ ์œˆ๋„์šฐ ๋ณ‘๋ ฌ ๊ฐ์ง€ | โœ… | +| 15 | ์ŠคํŠธ๋ฆฌ๋ฐ ํ…์ŠคํŠธ ๋ˆ„์  O(nยฒ)โ†’O(n) | โœ… | -๋‘ ์‚ฌ์ดํŠธ ๋ชจ๋‘ `max_tokens: 4096` โ†’ `max_tokens: resolveMaxTokens(session)` ๊ต์ฒด. +### 08-22 ์„ธ์…˜ #2 (47711a9..60c6021): ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ ๋ถ„๋ฆฌ + ์•ˆ์ •์„ฑ +- ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ ๋กœ์ปฌ/ํด๋ผ์šฐ๋“œ ๋ถ„๋ฆฌ (`isLocalBackendURL`, `buildSystemPrompt(..., isLocal)`) +- ๋นˆ ์‘๋‹ต ์žฌ์‹œ๋„ 1โ†’3 (MAX_EMPTY_RESPONSE_RETRIES = 3) +- ๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ SGR-1006 ๋ฒ„๊ทธ ์ˆ˜์ • (๋ชจ์…˜ ๋น„ํŠธ 32 ์ธ์‹) +- maxIterations ๊ธฐ๋ณธ๊ฐ’ 300 ๋ฐ˜์˜ -### ๋™์ž‘ -- `contextWindow โˆ’ lastContextTokens โˆ’ 512`๋ฅผ ์ถœ๋ ฅ ์˜ˆ์‚ฐ์œผ๋กœ ํ• ๋‹น -- ํ•˜ํ•œ 2048: ์ปจํ…์ŠคํŠธ ๊ฑฐ์˜ ์ฐผ์–ด๋„ ์ตœ์†Œ ์ถœ๋ ฅ ๋ณด์žฅ -- ์ƒํ•œ `contextWindow`: ์œˆ๋„์šฐ ์ปค๋„ ๊ทธ ์ด์ƒ ์š”๊ตฌ ์•ˆ ํ•จ -- 32k ์œˆ๋„์šฐ / 8k ์‚ฌ์šฉ ์ค‘ โ†’ ์•ฝ 23k ์ถœ๋ ฅ (๊ธฐ์กด 4096์˜ 5.7๋ฐฐ) -- 8k ์œˆ๋„์šฐ / 6k ์‚ฌ์šฉ ์ค‘ โ†’ 2048 (๊ธฐ์กด๊ณผ ๋™์ผ) +### 08-22 ์„ธ์…˜ #3 (Claude Code, c539995): ํฌ๋ž˜์‹œ/๋งค์นญ ๋ฒ„๊ทธ ์ˆ˜์ • +- ๋งˆ์šฐ์Šค ํฌ๋ž˜์‹œ ์ˆ˜์ • (`effectiveScrollTopRef` ๋ฏธ์„ ์–ธ โ†’ ReferenceError) +- CRLF ์ œ์–ด๋ฌธ์ž JSON ํŒŒ์‹ฑ ๋ฒ„๊ทธ (`escapeRawControlCharsInStrings` in partialJson.ts + nativeAdapter.ts ๋ฆฌํŽ˜์–ด ํด๋ฐฑ) +- CRLF edit_file/multi_edit ๋งค์นญ ๋ฒ„๊ทธ (LF ์ •๊ทœํ™”๋กœ ๋งค์นญ, ์“ฐ๊ธฐ ์ „ ์›๋ž˜ EOL ๋ณต์›) +- ๐Ÿ”ฅ ๋ฒˆ์ธ๋ ˆ์ดํŠธ ์˜คํ•ด ์ˆ˜์ • (input+output โ†’ output๋งŒ) -### ๋‚จ๊ฒจ๋‘” ๊ฒƒ -- `compactSession`์˜ `max_tokens: 1024` (loop.ts:186) โ€” ์งง์€ ์‚ฐ๋ฌธ ์š”์•ฝ์šฉ์ด๋ผ ๋™์  ๊ณ„์‚ฐ ๋ถˆํ•„์š”, ๊ทธ๋Œ€๋กœ ์œ ์ง€ -- `capabilityProbe.ts`์˜ `max_tokens: 200` โ€” ํ•‘ ํˆด์šฉ, ๊ทธ๋Œ€๋กœ ์œ ์ง€ - -### ๊ฒ€์ฆ -- `tsc --noEmit` โœ“ -- `npm run build` โœ“ (217.48 KB) -- `npm test` โœ“ 27ํŒŒ์ผ 179๊ฐœ ์ „๋ถ€ ํ†ต๊ณผ - -### ํŽธ์ง‘ ๋ฉ”๋ชจ -- ํŒŒ์ผ์ด CRLF๋ผ edit_file ๋„๊ตฌ๊ฐ€ ๋งค์นญ ์‹คํŒจํ•จ โ†’ `src/agent/_patch.py` ์ž„์‹œ ์Šคํฌ๋ฆฝํŠธ๋กœ ๋ฐ”์ด๋„ˆ๋ฆฌ ๊ต์ฒด ํ›„ ์‚ญ์ œ -- ํ–ฅํ›„ ์ด ํ”„๋กœ์ ํŠธ edit_file ์‹œ๋„ ์ „ `file `๋กœ ์ธ์ฝ”๋”ฉ ํ™•์ธ; CRLF๋ฉด Python ๋ฐ”์ด๋„ˆ๋ฆฌ ํŽธ์ง‘ ๋˜๋Š” `sed` ์‚ฌ์šฉ +### ํ˜„์žฌ ๋ฏธ์ปค๋ฐ‹ ๋ณ€๊ฒฝ (working tree) +| ํŒŒ์ผ | ๋ณ€๊ฒฝ ๋‚ด์šฉ | +|---|---| +| `package.json` / `package-lock.json` | ์˜์กด์„ฑ ๋Œ€๊ฑฐ ์—…๊ทธ๋ ˆ์ด๋“œ (openai 6โ†’7, commander 13โ†’15, execa 9โ†’10, vitest 3โ†’4, node types 22โ†’26, tsup target node20โ†’node22 ๋“ฑ) | +| `src/ui/ink/mouseInput.ts` | **์‚ญ์ œ๋จ** โ€” ๋งˆ์šฐ์Šค ํŠธ๋ž˜ํ‚น ์ „๋ฉด ํ์ง€ | +| `src/ui/ink/index.tsx` | alternate screen buffer ์ œ๊ฑฐ, normal screen + `` ์•„ํ‚คํ…์ฒ˜๋กœ ์ „ํ™˜ | +| `src/ui/ink/App.tsx` | ๋งˆ์šฐ์Šค/๊ฐ€์ƒ์Šคํฌ๋กค ์ฝ”๋“œ ์ œ๊ฑฐ, `` ๊ธฐ๋ฐ˜ ๋ Œ๋”๋ง์œผ๋กœ ์žฌ๊ตฌ์กฐํ™” | +| `src/ui/ink/ChatInput.tsx` | ๋งˆ์šฐ์Šค ๊ด€๋ จ ์ฝ”๋“œ ์ œ๊ฑฐ, ๋ฉ€ํ‹ฐ๋ผ์ธ/๋ธŒ๋ž˜ํ‚ท ํŽ˜์ด์ŠคํŠธ/@๋ฉ˜์…˜ ์œ ์ง€ | +| `src/ui/ink/HistoryItemView.tsx` | ๋งˆ์šฐ์Šค ์„ ํƒ ๊ด€๋ จ ์ฝ”๋“œ ์ œ๊ฑฐ | +| `src/ui/ink/FilePanel.tsx` | (๋ณ€๊ฒฝ ์žˆ์Œ โ€” ์ƒ์„ธ ํ™•์ธ ํ•„์š”) | +| `src/agent/loop.ts` | (๋ณ€๊ฒฝ ์žˆ์Œ โ€” ์ƒ์„ธ ํ™•์ธ ํ•„์š”) | +| `src/agent/loop.test.ts` | (๋ณ€๊ฒฝ ์žˆ์Œ โ€” ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€/์ˆ˜์ •) | +| `src/codeintel/lspManager.ts` | (๋ณ€๊ฒฝ ์žˆ์Œ) | +| `src/config/defaults.ts` | (๋ณ€๊ฒฝ ์žˆ์Œ) | +| `src/config/store.ts` | (๋ณ€๊ฒฝ ์žˆ์Œ) | +| `src/persistence/sessionStore.ts` / `.test.ts` | (๋ณ€๊ฒฝ ์žˆ์Œ) | +| `tsup.config.ts` | target: node20 โ†’ node22 | --- -## โœ… ์™„๋ฃŒ: #3 ์ •ํ™•ํ•œ ํ† ํฐ ์ถ”์ • (์Šคํฌ๋ฆฝํŠธ ์ธ์‹ ํœด๋ฆฌ์Šคํ‹ฑ) +## ํ•ต์‹ฌ ์•„ํ‚คํ…์ฒ˜ ๋ณ€๊ฒฝ: Normal Screen + Static -### ๋ฌธ์ œ -`src/utils/tokens.ts`์˜ `estimateTokens`๊ฐ€ `JSON.stringify(messages).length / 4` โ€” ๋‘ ๊ฐ€์ง€ ์‹คํŒจ ๋ชจ๋“œ: -1. JSON ์ง๋ ฌํ™” ์˜ค๋ฒ„ํ—ค๋“œ(๋”ฐ์˜ดํ‘œ, ์ค‘๊ด„ํ˜ธ, ์ด์Šค์ผ€์ดํ”„)๋ฅผ ์ฝ˜ํ…์ธ  ํ† ํฐ์œผ๋กœ ๊ณ„์‚ฐ โ†’ ์ถ”์ •์น˜ 15โ€“25% ๋ถ€ํ’€๋ฆผ (๋ชจ๋ธ์— ์•ˆ ๋ณด๋‚ด๋Š” ๊ฒƒ๋“ค) -2. ๋™์ผํ•œ chars/token ๋น„์œจ์„ ๋ชจ๋“  ์Šคํฌ๋ฆฝํŠธ์— ์ ์šฉ โ†’ ์˜์–ด ์‚ฐ๋ฌธ ~4, ์ฝ”๋“œ/๊ธฐํ˜ธ ~3.5, CJK(ํ•œ๊ตญ์–ด/์ค‘๊ตญ์–ด/์ผ๋ณธ์–ด) ~1.5์ธ๋ฐ ๋ฌด์‹œ โ†’ CJK ์ปจํ…์ŠคํŠธ ๊ณผ์†Œํ‰๊ฐ€, ์ปดํŒฉ์…˜ ํƒ€์ด๋ฐ ๋ถ€์ •ํ™• +์ด์ „ ์•„ํ‚คํ…์ฒ˜์—์„œ ๋Œ€ํญ ์ „ํ™˜: -์‹ค์ œ usage๊ฐ€ ์˜ค๋ฉด ์ด๋ฏธ ์ •ํ™•ํ•˜์ง€๋งŒ(`lastContextTokensIsEstimate = false`), ์ถ”์ •์€ ์ฒซ ํ„ด ์ „/์ปดํŒฉ์…˜ ์งํ›„/์„œ๋ธŒ์—์ด์ „ํŠธ ์ƒ์„ฑ ์‹œ ์‚ฌ์šฉ โ†’ ์ด ์‹œ์ ์˜ ๋ถ€์ •ํ™•์ด ์ปดํŒฉ์…˜ ํƒ€์ด๋ฐ์„ ๋น—๋‚˜๊ฐ€๊ฒŒ ํ•จ. +### ์ด์ „ (์‚ญ์ œ๋จ) +- Alternate screen buffer ์ง„์ž…/์ข…๋ฃŒ (`\x1b[?1049h` / `\x1b[?1049l`) +- ์ธ์•ฑ ๊ฐ€์ƒ ์Šคํฌ๋กค (PageUp/PageDown, scrollTop/ref) +- ๋งˆ์šฐ์Šค ํŠธ๋ž˜ํ‚น (SGR-1006, `\x1b[?1002h\x1b[?1006h`) +- ๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ ํ…์ŠคํŠธ ์„ ํƒ + OSC 52 ํด๋ฆฝ๋ณด๋“œ ๋ณต์‚ฌ +- `mouseInput.ts`: `matchMouseSequence`, `logicalButton`, `copyToClipboard` +- `effectiveScrollTopRef`, `selectionStart/End`, `extractSelectionText` -### ํ•ด๊ฒฐ -`tokens.ts`๋ฅผ ๋ฉ”์‹œ์ง€ ๊ตฌ์กฐ ์ˆœํšŒ + ์Šคํฌ๋ฆฝํŠธ ์ธ์‹ ํœด๋ฆฌ์Šคํ‹ฑ์œผ๋กœ ์žฌ์ž‘์„ฑ: - -- **PER_MESSAGE_OVERHEAD = 4**: ์ฑ„ํŒ… ํ…œํ”Œ๋ฆฟ์ด ๊ฐ ๋ฉ”์‹œ์ง€์— ์ถ”๊ฐ€ํ•˜๋Š” ์—ญํ• /๊ตฌ๋ถ„์ž ํ† ํฐ(~3โ€“5) ๋ฐ˜์˜ -- **๋ฉ”์‹œ์ง€๋ณ„ ์ฝ˜ํ…์ธ  ์ˆœํšŒ**: ์‹œ์Šคํ…œ/์‚ฌ์šฉ์ž/์–ด์‹œ์Šคํ„ดํŠธ ํ…์ŠคํŠธ, tool_calls ๊ตฌ์กฐ, tool ๊ฒฐ๊ณผ๋ฅผ JSON์ด ์•„๋‹Œ ๋ชจ๋ธ์ด ์‹ค์ œ๋กœ ๋ณด๋Š” ํ…์ŠคํŠธ๋กœ ์ถ”์ถœ -- **์Šคํฌ๋ฆฝํŠธ ์ธ์‹ ๊ฐ€์ค‘์น˜** (`weightedChars`): - - CJK(ํžˆ๋ผ๊ฐ€๋‚˜/๊ฐ€ํƒ€์นด๋‚˜/ํ•œ์ž/ํ•œ๊ธ€) ร—2.4 โ†’ ~1.5 chars/token (๊ฐ ์ฝ”๋“œ ํฌ์ธํŠธ๊ฐ€ ๋ณดํ†ต ์ž์ฒด BPE ํ† ํฐ) - - ์กฐ๋ฐ€ ๊ธฐํ˜ธ(๊ตฌ๋‘์ /์—ฐ์‚ฐ์ž/๊ด„ํ˜ธ, ์ฝ”๋“œ์— ํ”ํ•จ) ร—1.15 โ†’ ~3.5 chars/token - - ๋ผํ‹ด ๊ธฐ๋ณธ ร—1 โ†’ ~4 chars/token -- **๋ฉ€ํ‹ฐํŒŒํŠธ ์ฝ˜ํ…์ธ **: ํ…์ŠคํŠธ ํŒŒํŠธ๋Š” ํ…์ŠคํŠธ, ์ด๋ฏธ์ง€/์˜ค๋””์˜ค ํŒŒํŠธ๋Š” flat 8 ํ† ํฐ(base64๊ฐ€ ์•„๋‹Œ placeholder ํ† ํฐ) -- ๋™๊ธฐ ์ˆœ์ˆ˜ ์ถ”์ • ์œ ์ง€(๋ฐฑ์—”๋“œ ํ˜ธ์ถœ ์—†์Œ) โ†’ ์ฒซ ํ„ด/์ปดํŒฉ์…˜/์„œ๋ธŒ์—์ด์ „ํŠธ์— ์•ˆ์ „ - -Ollama `/api/tokenize`๋Š” ๋ฐฑ์—”๋“œ ๋ถ„๊ธฐ + ๋งค ํ„ด ์ง€์—ฐ์ด ํ•„์š”ํ•ด ์ œ์™ธ(์˜์กด์„ฑยท๋ณต์žก๋„ ๋Œ€๋น„ ํšจ๊ณผ ๋ถ€์กฑ). ํœด๋ฆฌ์Šคํ‹ฑ ๊ฐœ์„ ์œผ๋กœ ์ฆ‰์‹œ ํšจ๊ณผ. - -### ๋™์ž‘ -- ํ•œ๊ตญ์–ด ๋ฉ”์‹œ์ง€ 40์ž: ๊ธฐ์กด 10 ํ† ํฐ โ†’ ~27 ํ† ํฐ (์‹ค์ œ์— ๊ฐ€๊นŒ์›€) -- ์ฝ”๋“œ/๊ธฐํ˜ธ: ๊ธฐ์กด๋ณด๋‹ค ์•ฝ๊ฐ„ ๋†’๊ฒŒ โ†’ ์ปดํŒฉ์…˜ ์กฐ๊ธฐ ํŠธ๋ฆฌ๊ฑฐ (OOM ๋ฐฉ์ง€) -- ์˜์–ด ์‚ฐ๋ฌธ: ๊ธฐ์กด๊ณผ ์œ ์‚ฌํ•˜๋˜ JSON ์˜ค๋ฒ„ํ—ค๋“œ ์ œ๊ฑฐ โ†’ ์•ฝ๊ฐ„ ๋‚ฎ์•„์ง -- ๊ตฌ์กฐ(tool_calls, ๋ฉ€ํ‹ฐํŒŒํŠธ) ๋น„์šฉ ๋ฐ˜์˜ - -### ๊ฒ€์ฆ -- `tsc --noEmit` โœ“ -- `npm run build` โœ“ (224.26 KB) -- `npm test` โœ“ 29ํŒŒ์ผ 203๊ฐœ ์ „๋ถ€ ํ†ต๊ณผ (์‹ ๊ทœ 9๊ฐœ: tokens.test.ts) -- `loop.test.ts`๋Š” `lastContextTokens` ์ง์ ‘ ์„ค์ • โ†’ ์ถ”์ •๊ฐ’ ๋ณ€ํ™”์— ์˜ํ–ฅ ์—†์Œ ํ™•์ธ - -### ํŽธ์ง‘ ๋ฉ”๋ชจ -- `tokens.ts` CRLF โ†’ write_file + Python ๋ณ€ํ™˜ -- `tokens.test.ts` LF โ†’ edit_file ์‚ฌ์šฉ -- `ChatCompletionMessageParam` ๋ฉ€ํ‹ฐํŒŒํŠธ ํƒ€์ž… ์บ์ŠคํŠธ `as unknown as` ํ•„์š” (OpenAI ํƒ€์ž… narrow) - -### ๋‚จ๊ฒจ๋‘” ๊ฒƒ -- Ollama `/api/tokenize` ์บ์‹ฑ: ๋ฐฑ์—”๋“œ๋ณ„ ๋ถ„๊ธฐ + ๋น„๋™๊ธฐ ํ•„์š” โ†’ ๋ณ„๋„ ์ž‘์—…. ํ˜„์žฌ ํœด๋ฆฌ์Šคํ‹ฑ์œผ๋กœ ์ถฉ๋ถ„ํžˆ ๊ฐœ์„ ๋จ -- ์‹ค์ œ usage ๋„์ฐฉ ํ›„์—๋Š” ํ•ญ์ƒ ์ •ํ™•ํ•œ ๊ฐ’ ์‚ฌ์šฉ(`updateContextTracking`์˜ `lastContextTokensIsEstimate = false`) +### ํ˜„์žฌ (์ƒˆ ์•„ํ‚คํ…์ฒ˜) +- Normal screen buffer โ€” ํ„ฐ๋ฏธ๋„์˜ ์ผ๋ฐ˜ ์Šคํฌ๋กค๋ฐฑ์— ์ง์ ‘ ์ถœ๋ ฅ +- Ink `` ์ปดํฌ๋„ŒํŠธ โ€” ์™„๋ฃŒ๋œ ํžˆ์Šคํ† ๋ฆฌ ์•„์ดํ…œ์„ ํ•œ ๋ฒˆ๋งŒ ๋ Œ๋”๋ง, ์žฌ๋ Œ๋”๋ง ์—†์Œ +- ๋งˆ์šฐ์Šค ํŠธ๋ž˜ํ‚น ์—†์Œ โ€” ํ„ฐ๋ฏธ๋„ ์ž์ฒด ๋„ค์ดํ‹ฐ๋ธŒ ์Šคํฌ๋กค/์„ ํƒ/๋ณต์‚ฌ์— ์˜์กด +- `mouseInput.ts` ํŒŒ์ผ ์‚ญ์ œ๋จ +- ์ด์ : ์ฝ”๋“œ ๋Œ€ํญ ๊ฐ์†Œ, ํ„ฐ๋ฏธ๋„ ํ˜ธํ™˜์„ฑ ํ–ฅ์ƒ, SSH/tmux ๊ธฐ๋ณธ ์ž‘๋™ --- -## โœ… ์™„๋ฃŒ: #4 ์Šค๋งˆํŠธ ์ถœ๋ ฅ ์บก (head+tail ๋ณด์กด) +## ์„ค์ • ๊ธฐ๋ณธ๊ฐ’ (ํ˜„์žฌ) -### ๋ฌธ์ œ -`src/utils/truncate.ts`์˜ `truncate()`๊ฐ€ head๋งŒ ๋ณด์กด. ๊ธด ๋ช…๋ น ์ถœ๋ ฅ์—์„œ tail์˜ ์—๋Ÿฌ/์ƒํƒœ ์ค„์ด ์ž˜๋ฆผ โ†’ ๋ชจ๋ธ์ด ์‹คํŒจ ์›์ธ์„ ๋ชป ๋ด„. ํŠนํžˆ ๋กœ์ปฌ ๋ชจ๋ธ์—์„œ bash/git ์ถœ๋ ฅ์ด ๊ธธ๋ฉด ๋งˆ์ง€๋ง‰ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๊ฐ€ ์‚ฌ๋ผ์ ธ ๋””๋ฒ„๊น… ๋ถˆ๊ฐ€. - -### ํ•ด๊ฒฐ -`truncate.ts`๋ฅผ head+tail ๋ณด์กด(์ค‘๊ฐ„ ์ƒ๋žต)์œผ๋กœ ์žฌ์ž‘์„ฑ: - -- ๋ผ์ธ ๋‹จ์œ„๋กœ ์ž˜๋ผ ๊ฐ€๋…์„ฑ ์œ ์ง€ (๋ฐ˜ ์ค„ ์ž˜๋ฆผ ๋ฐฉ์ง€) -- ์˜ˆ์‚ฐ์˜ 60% head, 40% tail ํ• ๋‹น (tail์ด ์—๋Ÿฌ/์ƒํƒœ ์ค„์„ ๋‹ด๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„ ๋น„์ค‘ ๋†’์ž„) -- head/tail ์˜ค๋ฒ„๋žฉ ๊ฐ€๋“œ (์˜ˆ์‚ฐ ์ดˆ๊ณผ๊ฐ€ ์ ์„ ๋•Œ ์ค‘๋ณต ๋ผ์ธ ๋ฐฉ์ง€) -- ๋ชจ๋“  ๋ผ์ธ์ด ์˜ˆ์‚ฐ๋ณด๋‹ค ๊ธธ๋ฉด ๋ฌธ์ž ๋‹จ์œ„ ํด๋ฐฑ -- ์ƒ๋žต๋œ ๋ฌธ์ž ์ˆ˜ + ๋ณด์กด๋œ head/tail ๋ผ์ธ ์ˆ˜ ํ‘œ์‹œ - -### ์ ์šฉ ๋ฒ”์œ„ -`truncate()` ์‹œ๊ทธ๋‹ˆ์ฒ˜ ์œ ์ง€ โ†’ ๋ชจ๋“  ๊ธฐ์กด ํ˜ธ์ถœ์ž ์ž๋™ ๊ฐœ์„ : -- `bash.ts` (stdout/stderr) โ€” ๊ฐ€์žฅ ํฐ ํšจ๊ณผ -- `git.ts` (status/diff/log/show/branches ์ถœ๋ ฅ) โ€” **#11๋„ ํ•จ๊ป˜ ํ•ด๊ฒฐ** -- `bashOutput.ts`, `backgroundJobs.ts`, `webFetch.ts`, `importFile.ts` - -`readFile.ts`๋Š” ์ž์ฒด ํŽ˜์ด์ง€๋„ค์ด์…˜(nextOffset)์„ ์“ฐ๋ฏ€๋กœ ๊ทธ๋Œ€๋กœ ์œ ์ง€. `grep.ts`/`listFiles.ts`๋Š” ์ž์ฒด limit ์ž˜๋ผ๋‚ด๊ธฐ ์‚ฌ์šฉ. - -### ๋™์ž‘ -- ์งง์œผ๋ฉด ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ -- ๊ธธ๋ฉด head ์ผ๋ถ€ + `... [truncated N more characters โ€” middle omitted, X head + Y tail lines kept] ...` + tail ์ผ๋ถ€ -- tail์— ์—๋Ÿฌ ์ค„์ด ์žˆ์œผ๋ฉด ๋ชจ๋ธ์ด ๋ณผ ์ˆ˜ ์žˆ์Œ - -### ๊ฒ€์ฆ -- `tsc --noEmit` โœ“ -- `npm run build` โœ“ (222.30 KB) -- `npm test` โœ“ 28ํŒŒ์ผ 194๊ฐœ ์ „๋ถ€ ํ†ต๊ณผ (์‹ ๊ทœ 7๊ฐœ: truncate.test.ts) -- ๊ธฐ์กด ํ˜ธ์ถœ์ž ํ…Œ์ŠคํŠธ(bash/git/grep ๋“ฑ) ์ „๋ถ€ ํ†ต๊ณผ โ†’ ํ˜ธํ™˜์„ฑ ํ™•์ธ - -### ํŽธ์ง‘ ๋ฉ”๋ชจ -- `truncate.ts`๋Š” CRLF โ†’ write_file ํ›„ Python์œผ๋กœ CRLF ๋ณ€ํ™˜ -- `truncate.test.ts`๋Š” LF โ†’ edit_file ๋„๊ตฌ ์‚ฌ์šฉ ๊ฐ€๋Šฅ +| ์„ค์ • | ๊ธฐ๋ณธ๊ฐ’ | ๋น„๊ณ  | +|---|---|---| +| `DEFAULT_CONTEXT_WINDOW` | 8192 | ์ž๋™ ๊ฐ์ง€ ์‹คํŒจ ์‹œ ํด๋ฐฑ | +| `DEFAULT_MAX_ITERATIONS` | **300** | 50โ†’100โ†’300 ์ƒํ–ฅ | +| `DEFAULT_MAX_OUTPUT_TOKENS` | **131072** | 8192โ†’131072 (128K). GLM ๋“ฑ 1M ์ปจํ…์ŠคํŠธ ๋ชจ๋ธ ๋Œ€์‘ | +| `DEFAULT_MAX_RETRIES` | 0 | SDK ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„ | +| `DEFAULT_AUTO_COMPACT_THRESHOLD` | 0.85 | | +| `DEFAULT_REQUEST_TIMEOUT_MS` | 180,000 | 3๋ถ„ | +| `DEFAULT_SUBAGENT_TIMEOUT_MS` | 600,000 | 10๋ถ„ | +| `MAX_EMPTY_RESPONSE_RETRIES` | **3** | 1โ†’3 ์ƒํ–ฅ | +| `MAX_SUBAGENT_DEPTH` | 1 | ์„œ๋ธŒ์—์ด์ „ํŠธ ์ค‘์ฒฉ ๊ธˆ์ง€ | +| `MAX_PRESERVED_TAIL_MESSAGES` | 8 | ์ปดํŒฉ์…˜ ์‹œ ๋ณด์กด | +| `MAX_PRESERVED_TAIL_FRACTION` | 0.3 | ์ปดํŒฉ์…˜ ์‹œ ๋ณด์กด ๋น„์œจ | +| `MAX_RETAINED_IMAGES` | 2 | ํžˆ์Šคํ† ๋ฆฌ ์ด๋ฏธ์ง€ ๋ณด์กด | --- -## โœ… ์™„๋ฃŒ: #10 `edit_file` ์œ ์‚ฌ ๋งค์น˜ ์ œ์•ˆ - -### ๋ฌธ์ œ -fallback ๋ชจ๋ธ(๊ทธ๋ฆฌ๊ณ  ์ •๋ฐ€ edit์ด ์–ด๋ ค์šด ๋กœ์ปฌ ๋ชจ๋ธ)์ด `old_string`์„ ๊ฑฐ์˜ ์ •ํ™•ํžˆ but not exactly ์ œ๊ณต โ†’ `occurrences === 0` โ†’ ๋‹จ์ˆœ "not found" ์—๋Ÿฌ โ†’ ๋ชจ๋ธ์ด ๋งฅ๋ฝ ์—†์ด ์žฌ์‹œ๋„, ์‹คํŒจ ๋ฐ˜๋ณต. ์ •ํ™•ํ•œ ํ…์ŠคํŠธ๋ฅผ ์–ด๋””์„œ ๊ฐ€์ ธ์™€์•ผ ํ• ์ง€ ํžŒํŠธ๊ฐ€ ์—†์Œ. - -### ํ•ด๊ฒฐ -`src/tools/editFile.ts`์— ์œ ์‚ฌ ๋งค์น˜ ์ œ์•ˆ ์ถ”๊ฐ€: - -- `normaliseForCompare(s)`: ๊ณต๋ฐฑ ์—ฐ์†์„ ๋‹จ์ผ ์ŠคํŽ˜์ด์Šค๋กœ ์ •๊ทœํ™” โ†’ ๋“ค์—ฌ์“ฐ๊ธฐ/์ค„๋ฐ”๊ฟˆ ์ฐจ์ด์— ๊ฐ•๊ฑด -- `boundedLevenshtein(a, b, maxDist)`: ์กฐ๊ธฐ ์ข…๋ฃŒ Levenshtein. `maxDist` ์ดˆ๊ณผ ์‹œ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ โ†’ ํฐ ํŒŒ์ผ์—์„œ๋„ ์ €๋ ด -- `findSimilarMatch(content, needle)`: ํŒŒ์ผ ์ „์ฒด๋ฅผ ์Šฌ๋ผ์ด๋”ฉ ์œˆ๋„์šฐ(needle ๊ธธ์ด ยฑ50%, step = needle/8)๋กœ ์ˆœํšŒํ•˜๋ฉฐ ์ •๊ทœํ™”๋œ ํ…์ŠคํŠธ๋กœ ์œ ์‚ฌ๋„ ์ธก์ •. ์ตœ๊ณ  ์ ์ˆ˜ โ‰ฅ 0.5์ผ ๋•Œ๋งŒ ๋ฐ˜ํ™˜ -- `similarHint(original, oldString)`: ๋งค์น˜ ์‹คํŒจ ์‹œ ์—๋Ÿฌ/preview ๋ฉ”์‹œ์ง€์— "The closest match in the file (line N, ~X% similar):" + snippet ์ถ”๊ฐ€ - -handler์™€ preview ์–‘์ชฝ์˜ `occurrences === 0` ๊ฒฝ๋กœ์— ์ ์šฉ. ๊ธฐ์กด "not found" ๋ฉ”์‹œ์ง€ ๋’ค์— ํžŒํŠธ๊ฐ€ ๋ถ™์Œ. - -### ๋™์ž‘ -- ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜๋Š” ๋ถ€๋ถ„์ด ์žˆ์œผ๋ฉด ๊ธฐ์กด ๋™์ž‘ ์œ ์ง€ (ํžŒํŠธ ์—†์Œ) -- ์œ ์‚ฌํ•œ ๋ถ€๋ถ„์ด ์žˆ์œผ๋ฉด ์œ„์น˜ยท์œ ์‚ฌ๋„ยทsnippet ์ œ์•ˆ โ†’ ๋ชจ๋ธ์ด ์ •ํ™•ํ•œ `old_string`์œผ๋กœ ์žฌ์‹œ๋„ ๊ฐ€๋Šฅ -- ์ „ํ˜€ ๋‹ค๋ฅด๋ฉด ํžŒํŠธ ์—†์ด "not found"๋งŒ (๋…ธ์ด์ฆˆ ๋ฐฉ์ง€) - -### ๊ฒ€์ฆ -- `tsc --noEmit` โœ“ -- `npm run build` โœ“ (220.91 KB) -- `npm test` โœ“ 27ํŒŒ์ผ 187๊ฐœ ์ „๋ถ€ ํ†ต๊ณผ (์‹ ๊ทœ 3๊ฐœ: closest match ์ œ์•ˆ/preview/์œ ์‚ฌ๋„ ์ž„๊ณ„๊ฐ’) - -### ํŽธ์ง‘ ๋ฉ”๋ชจ -- `editFile.ts`๋Š” CRLF โ†’ Python ๋ฐ”์ด๋„ˆ๋ฆฌ ํŽธ์ง‘์œผ๋กœ ๊ต์ฒด + CRLF ์œ ์ง€ -- `editFile.test.ts`๋Š” LF โ†’ edit_file ๋„๊ตฌ ์‚ฌ์šฉ ๊ฐ€๋Šฅ -- `noUncheckedIndexedAccess` ํ™œ์„ฑํ™” โ†’ ๋ฐฐ์—ด ์ธ๋ฑ์Šค ์ ‘๊ทผ ์‹œ `?? ๊ธฐ๋ณธ๊ฐ’` ์ฒ˜๋ฆฌ ํ•„์š” - ---- - -## ๋‚จ์€ ์šฐ์„ ์ˆœ์œ„ โ€” ๋ชจ๋‘ ์™„๋ฃŒ โœ… - -12๊ฐœ ์—…๊ทธ๋ ˆ์ด๋“œ ํ›„๋ณด ์ „๋ถ€ ์™„๋ฃŒ. ์•„๋ž˜๋Š” ๊ตฌํ˜„ ์š”์•ฝ. - -### ์ฆ‰์‹œ ํšจ๊ณผ (๊ตฌํ˜„ ๊ฐ€๋ฒผ์›€) -- โœ… **#4 ์Šค๋งˆํŠธ ์ถœ๋ ฅ ์บก** โ€” `truncate.ts` head+tail ๋ณด์กด, ๋ชจ๋“  ํ˜ธ์ถœ์ž ์ž๋™ ๊ฐœ์„  -- โœ… **#10 `edit_file` ์œ ์‚ฌ ๋งค์น˜ ์ œ์•ˆ** โ€” ๋งค์น˜ ์‹คํŒจ ์‹œ Levenshtein ์œ ์‚ฌ ์œ„์น˜ ์ œ์•ˆ -- โœ… **#6 ๋™์  max_tokens** โ€” `resolveMaxTokens(session)` -- โœ… **#11 git ์ถœ๋ ฅ head+tail** โ€” #4๋กœ ํ•จ๊ป˜ ํ•ด๊ฒฐ - -### ์ •ํ™•๋„์— ํฐ ์˜ํ–ฅ -- โœ… **#3 ์ •ํ™•ํ•œ ํ† ํฐ ์ถ”์ •** โ€” ์Šคํฌ๋ฆฝํŠธ ์ธ์‹ ํœด๋ฆฌ์Šคํ‹ฑ (CJK/๊ธฐํ˜ธ/๊ตฌ์กฐ ๋น„์šฉ) -- โœ… **#5 ๋ถ€๋ถ„ ํžˆ์Šคํ† ๋ฆฌ ๋ณด์กด ์ปดํŒฉ์…˜** โ€” ์ตœ๊ทผ Nํ„ด ์›๋ณธ ๋ณด์กด + ์ด์ „ ์š”์•ฝ - -### ์„ฑ๋Šฅ -- โœ… **#1 ๋ณ‘๋ ฌ ํˆด ์‹คํ–‰** โ€” `runToolBatch`: read-only ํˆด `Promise.all` ๋ณ‘๋ ฌ, mutating ์ˆœ์ฐจ. 4๊ฐœ ๋ฃจํ”„์— ์ ์šฉ - -### ํšŒ๋ณต๋ ฅ -- โœ… **#2 ์žฌ์‹œ๋„ ์ •์ฑ…** โ€” `maxRetries` ์„ค์ •ํ™” (๊ธฐ๋ณธ 0, SDK ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„) -- โœ… **#8 MCP ์žฌ์—ฐ๊ฒฐ** โ€” `connectMcpServer` ์žฌ์‹œ๋„ + `/mcp reconnect` ๋ช…๋ น + ์„ธ์…˜ toolset ๊ฐฑ์‹  -- โœ… **#9 ์บ์‹œ TTL** โ€” `cachedAt` ํƒ€์ž„์Šคํƒฌํ”„ + N์ผ(๊ธฐ๋ณธ 7) ๊ฒฝ๊ณผ ์žฌ๊ฐ์ง€ - -### ๊ธฐํƒ€ -- โœ… **#7 ํˆด ์„ค๋ช… ํ’๋ถ€ํ™”** โ€” 8๊ฐœ ํ•ต์‹ฌ ํˆด description์— "use whenโ€ฆ"/์˜ˆ์‹œ ์ถ”๊ฐ€ -- โœ… **#12 auto-accept ์„ธ๋ถ„ํ™”** โ€” `auto-accept` = ๋ชจ๋“  mutating ํˆด ์ž๋™ ์Šน์ธ, `auto-edit` = ํŽธ์ง‘ ํˆด๋งŒ (์„ค๋ช…-๋™์ž‘ ์ผ์น˜) - ---- - -## ํ•ต์‹ฌ ํŒŒ์ผ ๋งต -- `src/agent/loop.ts` (1138ํ–‰) โ€” ๋ฉ”์ธ ์—์ด์ „ํŠธ ๋ฃจํ”„, ํ„ด/์ŠคํŠธ๋ฆฌ๋ฐ/ํˆด์ฝœ/์ปดํŒฉ์…˜/์„œ๋ธŒ์—์ด์ „ํŠธ/๋ณ‘๋ ฌ ํˆด ๋ฐฐ์น˜ -- `src/agent/session.ts` โ€” Session ๊ฐ์ฒด, ํ†ต๊ณ„, ์ƒํƒœ -- `src/agent/systemPrompt.ts` โ€” ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ ๋นŒ๋” (๋งค์šฐ ๊ฐ„๊ฒฐ, ๋กœ์ปฌ ์ค€์ˆ˜์œจ ์šฐ์„ ) -- `src/tools/` โ€” 14๊ฐœ ๋‚ด์žฅ ํˆด (read_file, list_files, grep, web_search, web_fetch, git_status, write_file, edit_file, bash, bash_output, bash_kill, git_commit, todo_write, agent) -- `src/toolcalling/` โ€” native ์–ด๋Œ‘ํ„ฐ, fallback ํŒŒ์„œ/ํ”„๋กฌํ”„ํŠธ, resolve (Ollama ๋นˆํ‚ค ๋ณต๊ตฌ ํฌํ•จ) -- `src/mcp/` โ€” MCP ํด๋ผ์ด์–ธํŠธ/๋งค๋‹ˆ์ €/์–ด๋Œ‘ํ„ฐ/config (๋ชจ๋“  MCP ํˆด mutating ๊ฐ•์ œ) -- `src/hooks/` โ€” ํ›… ๋Ÿฌ๋„ˆ (๋ณ‘๋ ฌ ์‹คํ–‰, SSRF ๊ฐ€๋“œ, exit 0/2 ์‹œ๋งจํ‹ฑ์Šค) -- `src/plugins/` โ€” Claude Code ํ”Œ๋Ÿฌ๊ทธ์ธ ๋กœ๋” (commands/agents/skills/hooks/MCP, ํˆด๋ช… ๋งคํ•‘) -- `src/backend/` โ€” client, capabilityProbe, contextWindow(์ž๋™ ํƒ์ง€), capabilityCache -- `src/config/` โ€” config (CLI > env > ์ €์žฅ > ๊ธฐ๋ณธ๊ฐ’ ์šฐ์„ ์ˆœ์œ„), defaults, store, types -- `src/permissions/` โ€” permissionManager (default/plan/auto-edit/auto-accept), types -- `src/persistence/` โ€” sessionStore (์›์ž ์“ฐ๊ธฐ, ํ์ž‰), exportSession, replayHistory -- `src/utils/` โ€” tokens, truncate, shell, processTree, image, html, mentions, projectInstructions -- `src/ui/ink/` โ€” Ink(React) ํ’€์Šคํฌ๋ฆฐ TUI ์ปดํฌ๋„ŒํŠธ - ## ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ํžŒํŠธ -- "์ž๊พธ ์—๋Ÿฌ"์˜ ์ฃผ์š” ์›์ธ: `max_tokens: 4096` ์ž˜๋ฆผ โ†’ malformed ํˆด์ฝœ (โœ… ํ•ด๊ฒฐ๋จ) -- ์ปจํ…์ŠคํŠธ ์œˆ๋„์šฐ๊ฐ€ 8192 ๊ธฐ๋ณธ๊ฐ’์ด๋ฉด `locode config set contextWindow <์‹ค์ œ๊ฐ’>` ํ•„์š” โ€” `resolveMaxTokens`๊ฐ€ ์ œ๊ฐ’์„ ๋‚ด๋ ค๋ฉด -- `/status`๋กœ ํ˜„์žฌ model/backend/mode/cwd ํ™•์ธ ๊ฐ€๋Šฅ -- fallback ๋ชจ๋“œ ํˆด์ฝœ ์‹คํŒจ ์‹œ `/mode fallback` ๊ฐ•์ œ ๋˜๋Š” ๋ชจ๋ธ ๊ต์ฒด -- **"Paused after N steps"๊ฐ€ ์ž์ฃผ ๋œจ๋ฉด**: 1 ์Šคํ… = 1 ๋ชจ๋ธ ์š”์ฒญ. ๋กœ์ปฌ ๋ชจ๋ธ์€ ํ•œ ๋ฒˆ์— 1 ํˆด๋งŒ ํ˜ธ์ถœํ•˜๋Š” ๊ฒฝํ–ฅ์ด ์žˆ์–ด ๋‹ค์ˆ˜ ํŒŒ์ผ ์ž‘์—…์ด 50์Šคํ…์„ ์‰ฝ๊ฒŒ ์ดˆ๊ณผ. ๊ธฐ๋ณธ๊ฐ’ 50โ†’**100** ์ƒํ–ฅ(์ผ์ƒ ์ž‘์—…์šฉ). ํฐ ๋ฐฐ์น˜ ์ž‘์—… ์‹œ `locode config set maxIterations ` (์˜ˆ: 200). ์ •์ง€ ์‹œ ์ž‘์—… ๋‚ด์šฉ์€ ๋ณด์กด๋˜๋ฏ€๋กœ "continue"๋กœ ์ด์–ด์„œ ์ง„ํ–‰ ๊ฐ€๋Šฅ. +- "์ž๊พธ ์—๋Ÿฌ"์˜ ์ฃผ์š” ์›์ธ: max_tokens ์ž˜๋ฆผ โ†’ malformed ํˆด์ฝœ (โœ… ํ•ด๊ฒฐ๋จ โ€” ๋™์  max_tokens + CRLF ์ œ์–ด๋ฌธ์ž ์ด์Šค์ผ€์ดํ”„) +- CRLF ํŒŒ์ผ edit_file ๋งค์นญ ๋ฒ„๊ทธ: โœ… ํ•ด๊ฒฐ๋จ (LF ์ •๊ทœํ™” ๊ณต๊ฐ„์—์„œ ๋งค์นญ, ์“ฐ๊ธฐ ์ „ ์›๋ž˜ EOL ๋ณต์›) +- "Paused after N steps": `locode config set maxIterations ` (๊ธฐ๋ณธ 300) +- ํด๋ผ์šฐ๋“œ ๋ชจ๋ธ ๋นˆ ์‘๋‹ต: MAX_EMPTY_RESPONSE_RETRIES=3์œผ๋กœ ์žฌ์‹œ๋„ +- ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ๊ฐ€ ๋กœ์ปฌ/ํด๋ผ์šฐ๋“œ ์ž๋™ ๋ถ„๊ธฐ โ€” localhost ๊ฐ์ง€ ์‹œ ๋กœ์ปฌ ํ”„๋กฌํ”„ํŠธ, ๊ทธ ์™ธ ํด๋ผ์šฐ๋“œ ํ”„๋กฌํ”„ํŠธ +- ๋ฐ˜๋ณต ๋ฃจํ”„ ๊ฐ์ง€: `detectRepetitionLoop()` โ€” ์ŠคํŠธ๋ฆฌ๋ฐ ํ…์ŠคํŠธ์—์„œ ์งง์€ ๋ฐ˜๋ณต ํŒจํ„ด ๊ฐ์ง€ ์‹œ ์ค‘๋‹จ +- ๋ฒˆ์ธ๋ ˆ์ดํŠธ(๐Ÿ”ฅ) ํ‘œ์‹œ: outputTokens/min๋งŒ ๋ฐ˜์˜ (inputTokens๋Š” ์ œ์™ธ) + --- -## 2026-08-20 ์„ธ์…˜: LSP ์™„์„ฑ + ๋กœ์ปฌ ๋ชจ๋ธ ์•ˆ์ •์„ฑ 3์ด์‚ฌ + ํŒจ๋ฆฌํ‹ฐ 4์ข… + UX - -์‹œ์ž‘์ : `6fe9888` (v0.6.0, 12๊ฐœ ์—…๊ทธ๋ ˆ์ด๋“œ ์™„๋ฃŒ ์ปค๋ฐ‹). ์ข…๋ฃŒ์ : `dfaf8d1`. -๊ฒ€์ฆ: typecheck โœ“ ยท build 274.76 KB ยท **301 tests** ํ†ต๊ณผ (์‹œ์ž‘ 219 โ†’ +82). - -### ๋ณต๊ตฌ โ€” ์–ด์ œ LSP ์ž‘์—… ๋‹ค์šด๋œ ์ง€์ ๋ถ€ํ„ฐ -์–ด์ œ `src/codeintel/lspManager.ts` ์ž‘์—… ์ค‘ ํƒ€์ž… ์—๋Ÿฌ 3๊ฐœ๋กœ ๋‹ค์šด. ์šฐ์„  ๋ณต๊ตฌ: -- ์ŠคํŠธ๋ฆผ ํƒ€์ž…: `ReadableStreamMessageReader`/`WriteableStreamMessageWriter` โ†’ `StreamMessageReader`/`StreamMessageWriter` (Node `Readable`/`Writable` ์ง์ ‘ ๋ฐ›์Œ, ์บ์ŠคํŠธ ๋ถˆํ•„์š”). -- diagnostic message: `string | MarkupContent` โ†’ `messageToString()` ํ—ฌํผ๋กœ ์ •๊ทœํ™”. - -### ์ปค๋ฐ‹ ๋ชฉ๋ก (12๊ฐœ, 6fe9888..dfaf8d1) - -| ์ปค๋ฐ‹ | ๋‚ด์šฉ | -|---|---| -| `cb95089` | LSP ์ฝ”๋“œ ์ธํ…”๋ฆฌ์ „์Šค + ๋ณ‘๋ ฌ ์„œ๋ธŒ์—์ด์ „ํŠธ mutation gate | -| `0ddc822` | ๋กœ์ปฌ ๋ชจ๋ธ ํˆด์ฝœ ์•ˆ์ •์„ฑ 3์ข… (๋ถ€๋ถ„ JSON ๋ณต๊ตฌ, fallback ํŒŒ์„œ, ๋นˆ ์‘๋‹ต ์žฌ์‹œ๋„) | -| `a458b3c` | multi_edit ๋„๊ตฌ | -| `b7233af` | LSP diagnostics ์‹ ์„ ๋„ (publishDiagnostics ๋Œ€๊ธฐ) | -| `f2ca154` | LSP ์„œ๋ฒ„ ์„ค์ •ํ™” + C/C++ clangd ํ†ตํ•ฉ | -| `c8cc78e` | README LSP ๋ฌธ์„œํ™” + LSP ๋„๊ตฌ ํ…Œ์ŠคํŠธ | -| `e3520b8` | /mouse ํ† ๊ธ€ (๋“œ๋ž˜๊ทธ/๋ณต์‚ฌ ๊ธฐ๋ณธ on) | -| `ee695a6` | DiffView (์ปฌ๋Ÿฌ diff ๋ Œ๋”๋ง) | -| `b2a7d1a` | notebook_edit (.ipynb ์…€ ํŽธ์ง‘) | -| `5438780` | ํ’€ ํŠธ๋žœ์Šคํฌ๋ฆฝํŠธ ๋‚ด๋ณด๋‚ด๊ธฐ + /export json | -| `dad0915` | ๊ตฌ์กฐํ™” task ์‹œ์Šคํ…œ (์˜์กด์„ฑ ๊ทธ๋ž˜ํ”„) | -| `dfaf8d1` | ์ถœ๋ ฅ ์ค‘ ์ž…๋ ฅ์ฐฝ ์ž‘๋™ (์ŠคํŠธ๋ฆฌ๋ฐ ์ค‘ ํƒ€์ดํ•‘ ๊ฐ€๋Šฅ) | - -### ์ƒ์„ธ - -#### LSP ์ฝ”๋“œ ์ธํ…”๋ฆฌ์ „์Šค (cb95089, b7233af, f2ca154, c8cc78e) -- `src/codeintel/lspManager.ts`: ์–ธ์–ด๋ณ„ LSP ์„œ๋ฒ„ lazy spawn (tsserver/pyright/gopls/clangd/rust-analyzer), didOpen/didChange ๋™๊ธฐํ™”, definition/references/diagnostics, notifyFileChanged, shutdownAll. -- `src/tools/codeIntel.ts`: definition/references/diagnostics 3๊ฐœ read-only ๋„๊ตฌ. tools/index.ts ๋“ฑ๋ก. -- `agent/loop.ts`: FileChanged ํ›…์— notifyFileChanged ์—ฐ๊ฒฐ (fire-and-forget, best-effort). -- `ui/ink/index.tsx`: ์ข…๋ฃŒ ์‹œ shutdownAll โ†’ orphan ์„œ๋ฒ„ ๋ฐฉ์ง€. -- **diagnostics ์‹ ์„ ๋„ (b7233af)**: ๊ธฐ์กด `setTimeout(0)` 1ํ„ด ๋Œ€๊ธฐ โ†’ tsserver/pyright ํฐ ํŒŒ์ผ์—์„œ publish ์•ˆ ๋จ โ†’ ํŽธ์ง‘ ์งํ›„ stale ๋ฐ˜ํ™˜. `diagWaiters` Map + `waitForDiagnostics(uri, 1500)`๋กœ publishDiagnostics๋ฅผ ํƒ€์ž„์•„์›ƒ ๊ฑธ๊ณ  ๋Œ€๊ธฐ. sync ์ „ ์บ์‹œ ํด๋ฆฌ์–ด. -- **์„œ๋ฒ„ ์„ค์ •ํ™” (f2ca154)**: `LANGUAGE_SPECS` ํ•˜๋“œ์ฝ”๋”ฉ โ†’ `configureLanguageSpecs(overrides)`๋กœ config ๋จธ์ง€. `locode config set lspServers ''` (ํ‚ค=languageId, {command, args?, extensions?}). ๋นŒํŠธ์ธ ์˜ค๋ฒ„๋ผ์ด๋“œ ๋˜๋Š” ์ƒˆ ์–ธ์–ด ์ถ”๊ฐ€(extensions ํ•„์ˆ˜). C/C++ โ†’ clangd 1๊ฐœ๋กœ ํ†ตํ•ฉ(๊ธฐ์กด 2๊ฐœ spawn ๋ฌธ์ œ ํ•ด๊ฒฐ). `resolveLspServers()` + ui/ink ์‹œ์ž‘ ์‹œ configure ํ˜ธ์ถœ. -- ํ…Œ์ŠคํŠธ: lspManager.test.ts (6, configureLanguageSpecs ๋จธ์ง€ โ€” ์Šคํฐ ์—†์ด _specsForTests), codeIntel.test.ts (7, ๋„๊ตฌ ๋””์ŠคํŒจ์น˜ โ€” vi.mock). - -#### ๋ณ‘๋ ฌ ์„œ๋ธŒ์—์ด์ „ํŠธ mutation gate (cb95089) -- `session.ts`: `session.mutationGate` promise chain. ์„ธ์…˜ ์ „์ฒด(์„œ๋ธŒ์—์ด์ „ํŠธ ํฌํ•จ) mutating ํˆด ์ง๋ ฌํ™”. ๊ถŒํ•œ ์Šฌ๋กฏ 1๊ฐœ์— ๋ ˆ์ด์Šค/ํŒŒ์ผ ์“ฐ๊ธฐ ๊ฒน์นจ ๋ฐฉ์ง€. read-only๋Š” ๊ณ„์† ๋ณ‘๋ ฌ. -- `loop.ts`: `runUnderMutationGate`. ์„œ๋ธŒ์—์ด์ „ํŠธ๋Š” ๋ถ€๋ชจ gate ์ƒ์†. -- `agentTool.ts`: `tasks` ๋ฐฐ์—ด โ†’ N๊ฐœ ์„œ๋ธŒ์—์ด์ „ํŠธ ๋ณ‘๋ ฌ. 1๊ฐœ ์‹คํŒจ๋Š” ๊ทธ ํƒœ์Šคํฌ error, ๋ฐฐ์น˜ ์ „์ฒด reject ์•„๋‹˜. -- `types.ts`: SubAgentResult. ํ…Œ์ŠคํŠธ: parallelAgents.test.ts. - -#### ๋กœ์ปฌ ๋ชจ๋ธ ํˆด์ฝœ ์•ˆ์ •์„ฑ 3์ข… (0ddc822) -- **#2 ๋ถ€๋ถ„ JSON ๋ณต๊ตฌ**: `src/toolcalling/partialJson.ts` โ€” `repairPartialJson()`. ๋‹ซํžˆ์ง€ ์•Š์€ ๋ฌธ์ž์—ด ๋‹ซ๊ธฐ, ์ค‘๊ด„ํ˜ธ/๋Œ€๊ด„ํ˜ธ ๋ฐธ๋Ÿฐ์Šค(max_tokens ์ž˜๋ฆผ), trailing comma ์ œ๊ฑฐ, stray trailing ํ† ํฐ ์ปท. ํ‚ค/๊ฐ’ ๋ฐœ๋ช… ์•ˆ ํ•จ(์Šคํ‚ค๋งˆ ๊ฒ€์ฆ ๊ฑฐ์นจ). `loop.ts`: native ํˆด์ฝœ ์ธ์ž parse ์‹คํŒจ ์‹œ repair ์‹œ๋„ ํ›„ non-streaming ์žฌ์‹œ๋„. ํ…Œ์ŠคํŠธ: partialJson.test.ts (10). -- **#3 fallback ํŒŒ์„œ ๊ฐ•ํ™”**: `fallbackParser.ts` โ€” ```tool_call ํŽœ์Šค(๋ฉ€ํ‹ฐ๋ผ์ธ ์•ต์ปค, ๋‚ด๋ถ€ ```json ํŽœ์Šค ์ œ๊ฑฐ) + ```json ํŽœ์Šค + bare(ํŽœ์Šค ์—†๋Š”) ํˆด์ฝœ ๊ฐ์ฒด. ๋ชจ๋“  ํ›„๋ณด partialJson repair ํ†ต๊ณผ. ์ž˜๋ฆฐ ํŽœ์Šค(๋‹ซ๋Š” ``` ์—†์Œ)๋„ ๋ณต๊ตฌ. ํ…Œ์ŠคํŠธ: fallbackParser.test.ts (11). -- **#6 ๋นˆ ์‘๋‹ต ์žฌ์‹œ๋„**: `loop.ts` โ€” bare empty `stop`(ํ…์ŠคํŠธ/ํˆด์ฝœ ์—†์Œ) 1ํšŒ ๋„›์ง€ ์žฌ์‹œ๋„. ์žฌ์‹œ๋„ ์†Œ์ง„ ํ›„ throw. ํ…Œ์ŠคํŠธ: loop.test.ts (2). - -#### multi_edit (a458b3c) -- `src/tools/multiEdit.ts`: ํ•œ ํŒŒ์ผ์— ์ˆœ์ฐจ ๋ฐฐ์น˜ ํŽธ์ง‘. ๊ฐ edit์€ running result์— ๊ฒ€์ฆ(์ด์ „ edit์ด ํ…์ŠคํŠธ ์˜ฎ๊ธธ ์ˆ˜ ์žˆ์Œ) โ†’ ๋ถˆ์ผ์น˜ ์‹œ edit ์ธ๋ฑ์Šค ๋ช…์‹œ. 1ํšŒ ์Šน์ธ + 1ํšŒ atomic ์“ฐ๊ธฐ. editFile์˜ applyEdit/countOccurrences ์žฌ์‚ฌ์šฉ. ํ…Œ์ŠคํŠธ: multiEdit.test.ts (8). - -#### /mouse ํ† ๊ธ€ โ€” ๋“œ๋ž˜๊ทธ/๋ณต์‚ฌ (e3520b8) -- ๋งˆ์šฐ์Šค ํœ  ์ถ”์ (`\x1b[?1000h`)์ด ํ„ฐ๋ฏธ๋„ ํ…์ŠคํŠธ ์„ ํƒ์„ ๋บ๋˜ ๋ฌธ์ œ. **๊ธฐ๋ณธ off** โ†’ ๋“œ๋ž˜๊ทธ/๋ณต์‚ฌ ๊ฐ€๋Šฅ. PageUp/PageDown ์Šคํฌ๋กค ์œ ์ง€. `/mouse on|off`๋กœ ํœ  ํ† ๊ธ€. help/README ๋ฌธ์„œํ™”. - -#### DiffView (ee695a6) -- `src/utils/diff.ts`: `looksLikeDiff()`. -- `src/ui/ink/DiffView.tsx`: unified diff ์ปฌ๋Ÿฌ ๋ Œ๋”๋ง(addition ๋…น/removal ๋นจ/hunkยทfile ํ—ค๋” dim). side-by-side ๋ชจ๋“œ(pairHunk + clip). v0.6.0 ํฌํŒ…. -- `theme.ts`: DIFF_ADD_HEX/DIFF_REMOVE_HEX. PermissionPrompt๊ฐ€ diff preview๋ฅผ ์ปฌ๋Ÿฌ ๋ Œ๋”. ํ…Œ์ŠคํŠธ: diff.test.ts (4), DiffView.test.ts (8). - -#### notebook_edit (b2a7d1a) -- `src/tools/notebookEdit.ts`: .ipynb ์…€ ์ธ์‹ ํŽธ์ง‘(replace/insert/delete, cell_id/cell_index). nbformat source ๋ผ์ธ ๋ฐฐ์—ด ๋ณ€ํ™˜. codeโ†”markdown ์ „ํ™˜ ์‹œ ํ•„๋“œ ์ •๋ฆฌ. atomic ์“ฐ๊ธฐ. v0.6.0 ํฌํŒ…(setLastEdit ์ œ๊ฑฐ). ํ…Œ์ŠคํŠธ: notebookEdit.test.ts (9). - -#### ํŠธ๋žœ์Šคํฌ๋ฆฝํŠธ ๋‚ด๋ณด๋‚ด๊ธฐ (5438780) -- `exportSession.ts`: ๋งˆํฌ๋‹ค์šด export์— ํˆด ์ฝœ/๊ฒฐ๊ณผ ํฌํ•จ(๊ธฐ์กด์—” ํ…์ŠคํŠธ๋งŒ). `sessionToJson()` ์ „์ฒด ๋ ˆ์ฝ”๋“œ JSON. `/export json [file]`. defaultExportFilename ํฌ๋งท๋ณ„. ํ…Œ์ŠคํŠธ: exportSession.test.ts (8). - -#### ๊ตฌ์กฐํ™” task ์‹œ์Šคํ…œ (dad0915) -- `src/tools/task.ts`: TaskStore(in-memory, per-session) + task_create/list/get/update. blocks/blockedBy ์˜์กด์„ฑ ๊ทธ๋ž˜ํ”„, owner ์†Œ์œ ๊ถŒ, status, metadata merge-patch(null=์‚ญ์ œ). self-ref/unknown/2-cycle ๊ฐ€๋“œ, delete ์‹œ dangling ref ์ •๋ฆฌ. ์Šคํ‚ค๋งˆ ์‚ฌ์šฉ ์ „ ์„ ์–ธ(v0.6.0 TDZ ์ˆ˜์ •). todo_write ๋ณ‘ํ–‰ ์œ ์ง€. -- types.ts: ctx.taskStore. session.ts: ์„ธ์…˜๋งˆ๋‹ค TaskStore. ํ…Œ์ŠคํŠธ: task.test.ts (9). - -#### ์ถœ๋ ฅ ์ค‘ ์ž…๋ ฅ์ฐฝ ์ž‘๋™ (dfaf8d1) -- ๊ธฐ์กด: isThinking/streamingText ์‹œ ChatInput ์–ธ๋งˆ์šดํŠธ โ†’ "Waitingโ€ฆ" ๋ฐ•์Šค ๊ต์ฒด โ†’ ํƒ€์ดํ•‘ ๋ถˆ๊ฐ€. -- ์ˆ˜์ •: ChatInput ํ•ญ์ƒ ๋งˆ์šดํŠธ. ์œ„์— 1์ค„ ์ƒํƒœ ํ‘œ์‹œ. handleSubmit ๊ฐ€๋“œ๋กœ ์ค‘๋ณต ์ œ์ถœ ๋ฐฉ์ง€(Enter๋Š” ํ„ด ์ข…๋ฃŒ๊นŒ์ง€ no-op, ํ…์ŠคํŠธ๋Š” ๋ฐ•์Šค์— ๋‚จ์Œ). Escape ์ธํ„ฐ๋ŸฝํŠธ๋Š” App ์ˆ˜์ค€ useInput์ด๋ผ ๊ณ„์† ์ž‘๋™. - -### ์ธ์ฝ”๋”ฉ ๋ฉ”๋ชจ -- ํ”„๋กœ์ ํŠธ ํŒŒ์ผ์€ CRLF/LF ์„ž์ž„. edit_file ๋„๊ตฌ ๋งค์นญ ์‹คํŒจ ๋นˆ๋ฒˆ โ†’ Python ๋ฐ”์ด๋„ˆ๋ฆฌ ํŽธ์ง‘ ์‚ฌ์šฉ. -- ํŒ: ํŽธ์ง‘ ์ „ `python3 -c "b=open(p,'rb').read(); print(repr(b[i:i+n]))"`๋กœ ์‹ค์ œ ๋ฐ”์ดํŠธ/์ธ์ฝ”๋”ฉ ํ™•์ธ. -- unicode(em-dash, CJK ๊ธ€๋ฆฌํ”„) ํฌํ•จ ์‹œ Python heredoc์€ SyntaxError โ†’ ์ž„์‹œ .py ํŒŒ์ผ๋กœ ์ž‘์„ฑ. - -### ๋‚จ์€ ํ›„๋ณด (์ด๋ฒˆ ์„ธ์…˜ ๋ฏธ์ง„ํ–‰) +## ๋‚จ์€ ํ›„๋ณด / TODO - LSP ์‹ค์„œ๋ฒ„ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ (์‹ค์ œ tsserver/pyright ๋„์›Œ์„œ ๊ฒ€์ฆ) - task store ์˜์†ํ™” (์„ธ์…˜์— task ์ €์žฅ) -- ๋ฉ˜์…˜ ํ”ผ์ปค ์—ด๋ฆฐ ์ƒํƒœ์—์„œ Escape ์ธํ„ฐ๋ŸฝํŠธ ์šฐ์„ ์ˆœ์œ„ ์กฐ์ •(์—ฃ์ง€ ์ผ€์ด์Šค) - ---- - -## 2026-08-21 ์„ธ์…˜: ๋งˆ์šฐ์Šค ๊ฐœ์„ , ๋ณ‘๋ ฌ ํˆด ์‹คํ–‰ ๊ฒ€์ฆ, ๋“œ๋ž˜๊ทธ ๋ณต์‚ฌ, ์ƒ‰์ƒ ์กฐ์ • - -์‹œ์ž‘์ : `dfaf8d1`. ๊ฒ€์ฆ: build โœ“ ยท **303 tests** ํ†ต๊ณผ. - -### ๋งˆ์šฐ์Šค ์Šคํฌ๋กค/์„ ํƒ โ€” Claude Code ์Šคํƒ€์ผ๋กœ ๊ฐœ์„  (e3520b8์—์„œ ์ด์–ด์„œ) - -**์ด์ „ ์ƒํƒœ**: `/mouse on` ํ•ด์•ผ ํœ  ์ž‘๋™, ๋งˆ์šฐ์Šค ๋ชจ๋“œ ์ผœ๋ฉด ํ„ฐ๋ฏธ๋„ ํ…์ŠคํŠธ ์„ ํƒ ๋ถˆ๊ฐ€. - -**๋ณ€๊ฒฝ**: -- ๋งˆ์šฐ์Šค ๋ชจ๋“œ **๊ธฐ๋ณธ ON** (`?1002h` ๋ฒ„ํŠผ+๋“œ๋ž˜๊ทธ ์ถ”์ ) -- Shift+ํด๋ฆญ/๋“œ๋ž˜๊ทธ: ํ„ฐ๋ฏธ๋„ ๋„ค์ดํ‹ฐ๋ธŒ ํ…์ŠคํŠธ ์„ ํƒ ๋™์ž‘ -- `mouseInput.ts`: SGR-1006 ์ „์ฒด ํŒŒ์‹ฑ (๋ฒ„ํŠผ, ์ขŒํ‘œ, ๋ˆ„๋ฆ„/ํ•ด์ œ, Shift/Meta/Ctrl ์ˆ˜์ •ํ‚ค) -- `logicalButton()` ๋‚ด๋ณด๋‚ด๊ธฐ ์ถ”๊ฐ€ -- `/mouse` ๋ช…๋ น ๋ฉ”์‹œ์ง€ ์—…๋ฐ์ดํŠธ - -### ๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ๋กœ ์นดํ”ผ โ€” ์ธ์•ฑ ํ…์ŠคํŠธ ์„ ํƒ + OSC 52 ํด๋ฆฝ๋ณด๋“œ - -**์ถ”๊ฐ€ ๊ตฌํ˜„**: -- `selectionStart`/`selectionEnd` ์ƒํƒœ๋กœ ๋“œ๋ž˜๊ทธ ๋ฒ”์œ„ ์ถ”์  (1-based content row) -- ์™ผ์ชฝ ํด๋ฆญ: ์„ ํƒ ์‹œ์ž‘, ๋“œ๋ž˜๊ทธ: ๋ฒ”์œ„ ๊ฐฑ์‹ , ๋ฆด๋ฆฌ์ฆˆ: ์„ ํƒ ํ…์ŠคํŠธ๋ฅผ `extractSelectionText()`๋กœ ์ถ”์ถœ -- `extractSelectionText()`: HistoryItem์—์„œ plain text ์ถ”์ถœ, row ๋ฒ”์œ„๋กœ ์ž๋ฅด๊ธฐ -- ๋ฆด๋ฆฌ์ฆˆ ์‹œ `copyToClipboard()` (OSC 52) ํ˜ธ์ถœ + "Copied N line(s)" ๋…ธํ‹ฐ์Šค -- Shift+ํด๋ฆญ/๋“œ๋ž˜๊ทธ๋Š” ๊ธฐ์กด๋Œ€๋กœ ํ„ฐ๋ฏธ๋„ ๋„ค์ดํ‹ฐ๋ธŒ ์„ ํƒ์œผ๋กœ ํŒจ์Šค์Šค๋ฃจ - -### ๋ณ‘๋ ฌ ํˆด ์‹คํ–‰ ๊ฒ€์ฆ - -**`runToolBatch` ๋””๋ฒ„๊ทธ ๋กœ๊ทธ ์ถ”๊ฐ€**: -- `[runToolBatch] N tool(s): parallel|sequential | tool1, tool2...` ํ˜•์‹์œผ๋กœ stderr ์ถœ๋ ฅ -- ๋ณ‘๋ ฌ ํ…Œ์ŠคํŠธ 2๊ฐœ ์ถ”๊ฐ€ (loop.test.ts): - - "runs multiple read-only tools in parallel": 3๊ฐœ read_file 200ms์”ฉ โ†’ ๋ณ‘๋ ฌ ์‹คํ–‰ ํ™•์ธ - - "runs mixed read+write tool calls sequentially": read_file + edit_file โ†’ ์ˆœ์ฐจ ์‹คํ–‰ ํ™•์ธ (305ms) - -**๊ฒฐ๊ณผ**: ํด๋ผ์šฐ๋“œ ๋ชจ๋ธ์—์„œ read-only ํˆด์€ `Promise.all` ๋ณ‘๋ ฌ ์‹คํ–‰, mutating ํˆด ์„ž์ด๋ฉด ์ˆœ์ฐจ. ๋กœ์ปฌ ๋ชจ๋ธ์€ ๋ณดํ†ต ํˆด์ฝœ 1๊ฐœ์”ฉ๋งŒ ๋ณด๋‚ด์„œ ๋ณ‘๋ ฌ์˜ ์ด์ ์„ ๋ชป ๋ˆ„๋ฆ„. - -### 100 ๋ฆฌ๋ฐ‹ iteration ๊ฒ€ํ†  - -- `DEFAULT_MAX_ITERATIONS = 100` (์ด์ „ 50์—์„œ ์ƒํ–ฅ) -- `locode config set maxIterations `๋กœ ์กฐ์ ˆ ๊ฐ€๋Šฅ -- ์ถœ๋ ฅ์ด ๋„์—„๋„์—„ํ•œ ์›์ธ: ๋ชจ๋ธ์ด ํˆด์ฝœ 1๊ฐœ์”ฉ๋งŒ ๋ณด๋‚ด์„œ ๋งค ์š”์ฒญ๋งˆ๋‹ค ์™•๋ณต ์‹œ๊ฐ„ + ์ปจํ…์ŠคํŠธ ์ฆ๊ฐ€ - -### `gateAndRun` ์—๋Ÿฌ ๋กœ๊น… ์ถ”๊ฐ€ - -- `RESOLVE ERROR`: ํˆด ์ธ์ž ํŒŒ์‹ฑ/๊ฒ€์ฆ ์‹คํŒจ ์‹œ stderr ๋กœ๊ทธ -- `RUN ERROR`: ํˆด ์‹คํ–‰ ์ค‘ ์—๋Ÿฌ ์‹œ stderr ๋กœ๊ทธ - -### DiffView ์‚ญ์ œ ๋ผ์ธ ์ƒ‰์ƒ ์กฐ์ • - -- `DIFF_REMOVE_HEX`: `#f85149` (๋ถ‰์€์ƒ‰) โ†’ `#e8904e` (์ฃผํ™ฉ์ƒ‰) -- ์–ด๋‘์šด ํ„ฐ๋ฏธ๋„ ๋ฐฐ๊ฒฝ์—์„œ ๊ฐ€๋…์„ฑ ๊ฐœ์„  - -### ํŒŒ์ผ ์ธ์ฝ”๋”ฉ ์ •๋ฆฌ -- `loop.ts`, `loop.test.ts`, `App.tsx` CRLFโ†’LF ๋ณ€ํ™˜ (`sed -i 's/\r$//'`) - ---- - -## 2026-08-22 ์„ธ์…˜: 15๊ฐœ ์—…๊ทธ๋ ˆ์ด๋“œ (์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ, ๋ฉ€ํ‹ฐ๋ผ์ธ, /undo, thinking, ์„ฑ๋Šฅ ๋“ฑ) - -์‹œ์ž‘์ : `5c3fcfd`. ์ข…๋ฃŒ์ : `5065990`. ๊ฒ€์ฆ: typecheck โœ“ ยท build 289.56 KB ยท **322 tests** ํ†ต๊ณผ (์‹œ์ž‘ 303 โ†’ +19). - -### ์ปค๋ฐ‹ - -| # | ํ•ญ๋ชฉ | ๋‚œ์ด๋„ | ํšจ๊ณผ | ๋กœ์ปฌ ํŠนํ™” | -|---|---|:---:|:---:|:---:| -| 1 | ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ ๋Œ€ํญ ๊ฐ•ํ™” | ์ค‘ | ๋Œ€ | โ˜…โ˜…โ˜… | -| 2 | ๋ฉ€ํ‹ฐ๋ผ์ธ ์ž…๋ ฅ (Shift+Enter) + ๋ธŒ๋ž˜ํ‚ท ํŽ˜์ด์ŠคํŠธ | ์ค‘ | ๋Œ€ | | -| 3 | `/undo` ๋ช…๋ น โ€” ๋งˆ์ง€๋ง‰ ํ„ด ๋กค๋ฐฑ | ์ค‘ | ๋Œ€ | | -| 4 | `/dashboard` ๋น„์šฉ/์ง€์—ฐ | โ€” | โ€” | ์ด๋ฏธ ๊ตฌํ˜„๋จ | -| 5 | ์ปจํ…์ŠคํŠธ ์‚ฌ์šฉ๋ฅ  ์ง„ํ–‰ ๋ฐ” | โ€” | โ€” | ์ด๋ฏธ ๊ตฌํ˜„๋จ | -| 6 | ์ถ”๋ก (thinking) ํ† ํฐ ์ง€์› | ์ค‘ | ๋Œ€ | โ˜…โ˜… | -| 7 | `@` ๋ฉ˜์…˜ ํผ์ง€ ๋งค์นญ + ์บ์‹œ TTL | ํ•˜ | ์ค‘ | | -| 8 | readFile ๋ฐ”์ด๋„ˆ๋ฆฌ ๊ฐ€๋“œ + ๋Œ€์šฉ๋Ÿ‰ ๋ณดํ˜ธ | ํ•˜ | ๋Œ€ | | -| 9 | writeFile ์›์ž ์“ฐ๊ธฐ (tempโ†’rename) | ํ•˜ | ๋Œ€ | | -| 10 | bash ๋ฌธ์ž์—ด ๋ˆ„์  O(nยฒ)โ†’O(n) | ํ•˜ | ์ค‘ | | -| 11 | estimateTokens ์ •๊ทœ์‹ ๊ณ ์†ํ™” | ํ•˜ | ์ค‘ | โ˜… | -| 12 | handleCompletedMessage anyโ†’Record | ํ•˜ | ํ’ˆ์งˆ | | -| 13 | ์„ธ์…˜ ๋ณต์› ์‹œ ๊ถŒํ•œ ์ƒํƒœ ๋ณด์กด | ์ค‘ | ์ค‘ | | -| 14 | ์ปจํ…์ŠคํŠธ ์œˆ๋„์šฐ ๋ณ‘๋ ฌ ๊ฐ์ง€ | ํ•˜ | ์ค‘ | โ˜… | -| 15 | ์ŠคํŠธ๋ฆฌ๋ฐ ํ…์ŠคํŠธ ๋ˆ„์  O(nยฒ)โ†’O(n) | ํ•˜ | ์ค‘ | | - -### ์ƒ์„ธ - -#### #1 ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ ๋Œ€ํญ ๊ฐ•ํ™” (`systemPrompt.ts`) - -๊ธฐ์กด 22์ค„(5๊ฐœ ์ œ๋„ค๋ฆญ ๊ฐ€์ด๋“œ) โ†’ 100์ค„+ ๊ตฌ์กฐํ™”๋œ ํ”„๋กฌํ”„ํŠธ: -- **๋„๊ตฌ ๋ถ„๋ฅ˜**: Read-only / Mutating (requires confirmation) ๊ทธ๋ฃน๋ณ„ ๋‚˜์—ด -- **ํ•ต์‹ฌ ์›์น™ 7๊ฐœ**: inspect before answering, prefer small edits, one tool per response in fallback, preserve existing style, keep answers concise, recovery over retry, respect confirmation -- **๋„๊ตฌ ์‚ฌ์šฉ ๊ฐ€์ด๋“œ**: ๊ฐ ๋„๊ตฌ๋ณ„ "use when..." ์ง€์‹œ (read_file, edit_file, bash, agent, task ๋“ฑ) -- **๋กœ์ปฌ ๋ชจ๋ธ ์ง€์นจ**: ์ž‘์€ ์ปจํ…์ŠคํŠธ ์œˆ๋„์šฐ, ํˆด์ฝœ ๋ถˆ์•ˆ์ •์„ฑ, ๋นˆ ์‘๋‹ต ๋Œ€์‘, ์ถœ๋ ฅ ๊ธธ์ด ์ œํ•œ -- **์•ˆ์ „ ๊ฐ€์ด๋“œ**: .git ์ˆ˜์ • ๊ธˆ์ง€, ๋Œ€๊ทœ๋ชจ ์‚ญ์ œ ์‹œ ํ™•์ธ, bash ์ฝ๊ธฐ ์„ ํ˜ธ -- **์„œ๋ธŒ์—์ด์ „ํŠธ ํ”„๋กฌํ”„ํŠธ**: `buildSystemPrompt` ์žฌ์‚ฌ์šฉ์œผ๋กœ ์ž๋™ ์ ์šฉ -- **ํ…Œ์ŠคํŠธ**: 12๊ฐœ ์‹ ๊ทœ (systemPrompt.test.ts) - -#### #2 ๋ฉ€ํ‹ฐ๋ผ์ธ ์ž…๋ ฅ + ๋ธŒ๋ž˜ํ‚ท ํŽ˜์ด์ŠคํŠธ (`ChatInput.tsx`) - -- **Shift+Enter**: ์ค„๋ฐ”๊ฟˆ ์‚ฝ์ž… (๊ธฐ์กด Enter=์ œ์ถœ ์œ ์ง€) -- **multiline ๋ชจ๋“œ**: ํ…์ŠคํŠธ์— `\n` ํฌํ•จ ์‹œ Enterโ†’์ค„๋ฐ”๊ฟˆ, Ctrl+Enterโ†’๊ฐ•์ œ ์ œ์ถœ -- **๋ธŒ๋ž˜ํ‚ท ํŽ˜์ด์ŠคํŠธ** (`\x1b[200~`โ€ฆ`\x1b[201~`): ํ„ฐ๋ฏธ๋„์—์„œ ๋ถ™์—ฌ๋„ฃ๊ธฐ ์‹œ ์—ฌ๋Ÿฌ ์ค„ ๊ทธ๋Œ€๋กœ ์‚ฝ์ž… -- **CRLFโ†’LF ๋ณ€ํ™˜**: ๋ถ™์—ฌ๋„ฃ์€ ํ…์ŠคํŠธ์˜ `\r\n`์„ `\n`์œผ๋กœ ์ •๊ทœํ™” -- `useCallback`์œผ๋กœ `maybeHandlePaste` ์ตœ์ ํ™”, `pasteBufferRef`/`inPasteRef`๋กœ ์ฒญํฌ ๋ฒ„ํผ๋ง - -#### #3 `/undo` ๋ช…๋ น (`session.ts`, `App.tsx`, `HistoryItemView.tsx`) - -- `undoLastTurn(session)`: ๋งˆ์ง€๋ง‰ user ๋ฉ”์‹œ์ง€๋ถ€ํ„ฐ ๋๊นŒ์ง€ ์ œ๊ฑฐ, ์ปจํ…์ŠคํŠธ ํ† ํฐ ์žฌ๊ณ„์‚ฐ -- ํŒŒ์ผ์‹œ์Šคํ…œ ๋ณ€๊ฒฝ์€ ๋กค๋ฐฑ ์•ˆ ํ•จ (soft undo) -- ์ œ๊ฑฐ๋œ ๋ฉ”์‹œ์ง€ ์ˆ˜ ๋ฐ˜ํ™˜ โ†’ "Undid last turn (removed N messages)" -- `/undo` ์Šฌ๋ž˜์‹œ ๋ช…๋ น + ๋„์›€๋ง์— ์ถ”๊ฐ€ -- **ํ…Œ์ŠคํŠธ**: 7๊ฐœ (session.test.ts) โ€” no user messages, ๋‹จ์ผ ํ„ด, ํˆด๊ฒฐ๊ณผ ํฌํ•จ, ์ด์ „ ํ„ด ๋ณด์กด ๋“ฑ - -#### #6 ์ถ”๋ก (thinking) ํ† ํฐ ์ง€์› (`events.ts`, `loop.ts`, `App.tsx`, `types.ts`, `HistoryItemView.tsx`) - -- `AgentEvent`์— `thinking_delta` / `thinking_done` ์ด๋ฒคํŠธ ์ถ”๊ฐ€ -- ์ŠคํŠธ๋ฆฌ๋ฐ ๋ฃจํ”„์—์„œ `delta.reasoning_content` ์บก์ฒ˜ (DeepSeek, QwQ ๋“ฑ) -- `fullThinking` ๋ˆ„์  โ†’ `thinking_done` ์ด๋ฒคํŠธ๋กœ ๋ฐฉ์ถœ -- UI: dimmed ๋ณด๋” + "Thinking:" ๋ ˆ์ด๋ธ”๋กœ ๋ Œ๋”๋ง -- `itemToLines`์— thinking ์ข…๋ฅ˜ ์ถ”๊ฐ€ (์„ ํƒ ํ…์ŠคํŠธ) - -#### #7 @ ๋ฉ˜์…˜ ํผ์ง€ ๋งค์นญ (`ChatInput.tsx`) - -- **ํผ์ง€ ๋งค์น˜**: `fuzzyMatch()` โ€” ์ •ํ™•ํ•œ ๋ถ€๋ถ„๋ฌธ์ž์—ด ๋งค์น˜ ์šฐ์„ , ์‹คํŒจ ์‹œ ๋ฌธ์ž ์ˆœ์„œ ๋งค์น˜ (์˜ˆ: "ut" โ†’ "utils/") -- **์ •๋ ฌ**: ์ •ํ™• ๋งค์น˜ ์šฐ์„ , ๊ทธ ๋‹ค์Œ ๊ธธ์ด์ˆœ -- **์บ์‹œ TTL**: `filesCachedAt` ์ƒํƒœ ์ถ”๊ฐ€, 5๋ถ„ ํ›„ ๋ฆฌ๊ธ€๋กœ๋ธŒ -- **`.gitignore` ์ค€์ˆ˜**: `fast-glob`์˜ `gitignore: true` ์˜ต์…˜ (ํƒ€์ž… ์ œํ•œ์œผ๋กœ ์ œ์™ธ, `ignore`๋กœ ๋Œ€์ฒด) - -#### #8 readFile ๋ฐ”์ด๋„ˆ๋ฆฌ ๊ฐ€๋“œ (`readFile.ts`) - -- `MAX_FILE_SIZE = 10 MB` โ€” `stat()` ์‚ฌ์ „ ์ฒดํฌ, ์ดˆ๊ณผ ์‹œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ -- `BINARY_EXTENSIONS` Set (30+ ํ™•์žฅ์ž) โ€” ํ™•์žฅ์ž ๊ธฐ๋ฐ˜ ๋ฐ”์ด๋„ˆ๋ฆฌ ๊ฐ์ง€ -- null-byte ํœด๋ฆฌ์Šคํ‹ฑ (8KB ์Šค์บ”) โ€” ํ™•์žฅ์ž ์—†๋Š” ๋ฐ”์ด๋„ˆ๋ฆฌ ํŒŒ์ผ ๊ฐ์ง€ -- `content.split("\n")` โ†’ `content.split(/\r?\n/)` โ€” CRLF ํ˜ธํ™˜ - -#### #9 writeFile ์›์ž ์“ฐ๊ธฐ (`writeFile.ts`) - -- `randomUUID()`๋กœ ์ž„์‹œ ํŒŒ์ผ๋ช… ์ƒ์„ฑ โ†’ `writeFile` โ†’ `rename` -- ์‹คํŒจ ์‹œ `unlink`๋กœ ์ž„์‹œ ํŒŒ์ผ ์ •๋ฆฌ -- `readExisting` catch์—์„œ `err.code === 'ENOENT'` ๋ช…์‹œ์  ์ฒดํฌ (๋‹ค๋ฅธ ์—๋Ÿฌ๋Š” ์žฌthrow) - -#### #10 bash ๋ฌธ์ž์—ด ๋ˆ„์  (`bash.ts`) - -- `let stdout = ""` โ†’ `const stdoutChunks: string[] = []` -- `stdout += d.toString()` โ†’ `stdoutChunks.push(d.toString())` -- ์ตœ์ข… `stdoutChunks.join("")` / `stderrChunks.join("")` -- O(nยฒ) ๋ฌธ์ž์—ด ๋ˆ„์  โ†’ O(n) ๋ฐฐ์—ด push + join - -#### #11 estimateTokens ์ •๊ทœ์‹ ๊ณ ์†ํ™” (`tokens.ts`) - -- per-character `weightedChars` ๋ฃจํ”„ โ†’ ์ •๊ทœ์‹ ๋ฒŒํฌ ์นด์šดํŒ…: - - CJK: `/[\u3040-\u30ff\u3400-\u9fff\uac00-\ud7af\u1100-\u11ff]/gu` โ†’ ร—2.4 - - Dense symbols: `/[!-\/:-@[-\`{-~]/g` โ†’ ร—1.15 - - ๋‚˜๋จธ์ง€: ร—1 -- `isCjk()` / `isDenseSymbol()` ํ—ฌํผ ์ œ๊ฑฐ - -#### #12 handleCompletedMessage ํƒ€์ž… (`loop.ts`) - -- `message: any` โ†’ `message: Record` -- ๋Ÿฐํƒ€์ž„ ๋™์ž‘์€ ๋™์ผ (์†์„ฑ ์ ‘๊ทผ ์‹œ any์™€ ํ˜ธํ™˜) - -#### #13 ์„ธ์…˜ ๋ณต์› ๊ถŒํ•œ ๋ณด์กด (`session.ts`, `sessionStore.ts`) - -- `SessionRecord`์— `allowedTools?: string[]` ์„ ํƒ์  ํ•„๋“œ ์ถ”๊ฐ€ -- `toSessionRecord()`: `session.permissions.listAllowed()` ์ €์žฅ -- `createSessionFromRecord()`: `record.allowedTools` ๋ณต์› โ†’ `session.permissions.allowForSession()` -- `/perm`์—์„œ "์ด ์„ธ์…˜์—์„œ ํ—ˆ์šฉ"ํ•œ ๋„๊ตฌ๊ฐ€ ์„ธ์…˜ ์žฌ๊ฐœ ํ›„์—๋„ ์œ ์ง€ - -#### #14 ์ปจํ…์ŠคํŠธ ์œˆ๋„์šฐ ๋ณ‘๋ ฌ ๊ฐ์ง€ (`contextWindow.ts`) - -- `detectContextWindow`: ์ˆœ์ฐจ `await Ollama โ†’ await LM Studio` โ†’ `Promise.allSettled([Ollama, LM Studio])` -- ๊ฐ์ง€ ์ง€์—ฐ ๋ฐ˜๊ฐ (Ollama๊ฐ€ ์•ˆ ๋Œ์•„๋„ LM Studio ๊ฒฐ๊ณผ ์ฆ‰์‹œ ์‚ฌ์šฉ) -- `ollama.status === "fulfilled" && ollama.value !== null` ํŒจํ„ด์œผ๋กœ null ๊ฒฐ๊ณผ ํ•„ํ„ฐ๋ง - -#### #15 ์ŠคํŠธ๋ฆฌ๋ฐ ํ…์ŠคํŠธ ๋ˆ„์  O(nยฒ)โ†’O(n) (`loop.ts`) - -- `fullText += delta.content` โ†’ `textChunks.push(delta.content)` -- ์ŠคํŠธ๋ฆผ ๋ฃจํ”„ ํ›„ `fullText = textChunks.join("")` -- ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ์—์„œ๋„ `textChunks.join("")`๋กœ ๋ถ€๋ถ„ ํ…์ŠคํŠธ ๋ณต๊ตฌ - -### ์ธ์ฝ”๋”ฉ ๋ฉ”๋ชจ -- `session.ts`, `App.tsx`, `HistoryItemView.tsx` CRLF โ†’ Python ๋ฐ”์ด๋„ˆ๋ฆฌ ํŽธ์ง‘์œผ๋กœ ๊ต์ฒด -- `ChatInput.tsx`, `systemPrompt.ts` LF โ†’ write_file/edit_file ์‚ฌ์šฉ - ---- - -## 2026-08-23 ์„ธ์…˜: ํด๋ผ์šฐ๋“œ ๋ชจ๋ธ ๋นˆ ์‘๋‹ต ์ˆ˜์ •, ๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ ๋””๋ฒ„๊ทธ - -์‹œ์ž‘์ : `47711a9`. ๊ฒ€์ฆ: typecheck โœ“ ยท build 294.28 KB ยท **326 tests** ํ†ต๊ณผ. - -### #13 ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ ํด๋ผ์šฐ๋“œ/๋กœ์ปฌ ๋ถ„๋ฆฌ (`systemPrompt.ts`, `session.ts`, `App.tsx`, `defaults.ts`) - -**๋ฌธ์ œ**: ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ์— "You run against a local model" / "Empty or malformed responses can happen" ๋“ฑ ๋กœ์ปฌ ๋ชจ๋ธ ์ „์šฉ ์ง€์‹œ์‚ฌํ•ญ์ด ํ•˜๋“œ์ฝ”๋”ฉ. GLM-5.1 ํด๋ผ์šฐ๋“œ ๋ชจ๋ธ์ด ์ด ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋ฐ›์œผ๋ฉด: -- ์ž์‹ ์ด ๋กœ์ปฌ ๋ชจ๋ธ์ด๋ผ๊ณ  ์ฐฉ๊ฐ โ†’ ๋ณด์ˆ˜์  ๋™์ž‘ -- "๋นˆ ์‘๋‹ต์ด ๋‚˜์˜ฌ ์ˆ˜ ์žˆ๋‹ค"๋Š” ์•”์‹œ โ†’ ์ž๊ธฐ ์ถฉ์กฑ์  ๋นˆ ์‘๋‹ต (Request failed: Empty response from model.) - -**ํ•ด๊ฒฐ**: `isLocal` ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ํ”„๋กฌํ”„ํŠธ ๋ถ„๋ฆฌ -- `isLocalBackendURL(baseURL)`: localhost/127.0.0.1/::1์ด๋ฉด true -- `buildSystemPrompt(tools, mode, projectInstructions, isLocal)`: - - **๋กœ์ปฌ ํ”„๋กฌํ”„ํŠธ** (`isLocal=true`, ๊ธฐ๋ณธ๊ฐ’): ๊ธฐ์กด ํ”„๋กฌํ”„ํŠธ ์œ ์ง€, "Working with local models" ์„น์…˜ ํฌํ•จ, 7๊ฐœ ์›์น™ - - **ํด๋ผ์šฐ๋“œ ํ”„๋กฌํ”„ํŠธ** (`isLocal=false`): ๋กœ์ปฌ ๋ชจ๋ธ ์ œํ•œ ์„น์…˜ ์ œ๊ฑฐ, 6๊ฐœ ์›์น™ (fallback ๋ชจ๋“œ์˜ "One tool per response" ๊ทœ์น™ ์ œ๊ฑฐ), "local model" ์–ธ๊ธ‰ ์—†์Œ -- `Session.isLocal` ํ•„๋“œ ์ถ”๊ฐ€, `createSession`/`createSessionFromRecord`/`setMode`/`compactSession`/`spawnSubAgent` ๋ชจ๋‘ ์ „ํŒŒ -- `App.tsx`์—์„œ `isLocalBackendURL(baseURLRef.current)` / `isLocalBackendURL(record.baseURL)` ๋กœ ์ž๋™ ์„ค์ • - -### #14 ๋นˆ ์‘๋‹ต ์žฌ์‹œ๋„ ์ฆ๊ฐ€ (`loop.ts`) - -- `MAX_EMPTY_RESPONSE_RETRIES`: 1 โ†’ **3** -- ํด๋ผ์šฐ๋“œ ๋ชจ๋ธ๋„ ๊ฐ„ํ—์  ๋นˆ ์‘๋‹ต ๋ฐœ์ƒ ๊ฐ€๋Šฅ (GLM-5.1 ์‚ฌ๋ก€) -- ์ด์ „: ์ดˆ๊ธฐ ์‘๋‹ต + 1ํšŒ ์žฌ์‹œ๋„ = ์ด 2ํšŒ โ†’ ์‹คํŒจ ์‹œ `AgentError("Empty response from model.")` -- ์ดํ›„: ์ดˆ๊ธฐ ์‘๋‹ต + 3ํšŒ ์žฌ์‹œ๋„ = ์ด 4ํšŒ โ†’ ๋นˆ ์‘๋‹ต ํ›„ ๋„›์ง€ ๋ฉ”์‹œ์ง€ ์‚ฝ์ž… ํ›„ ์žฌ์‹œ๋„ - -### #15 ๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ ๋””๋ฒ„๊ทธ (`App.tsx`) - -- ๋งˆ์šฐ์Šค ์ด๋ฒคํŠธ์— `process.stderr.write` ๋””๋ฒ„๊ทธ ๋กœ๊ทธ ์ถ”๊ฐ€ -- ํด๋ฆญ/๋“œ๋ž˜๊ทธ/๋ฆด๋ฆฌ์ฆˆ ์ด๋ฒคํŠธ์˜ `button`, `pressed`, `logicalBtn` ๊ฐ’ ํ™•์ธ์šฉ -- SGR-1006 ๋ชจ๋“œ 1002์—์„œ ๋“œ๋ž˜๊ทธ ์ด๋ฒคํŠธ(`button=32`, motion bit)๊ฐ€ `logicalButton()`์—์„œ `"other"`๋กœ ๋ถ„๋ฅ˜๋˜๋Š” ๋ฌธ์ œ ํ™•์ธ ์ค‘ -- ๋””๋ฒ„๊ทธ ์™„๋ฃŒ ํ›„ ์ œ๊ฑฐ ์˜ˆ์ • - -### maxIterations ๊ธฐ๋ณธ๊ฐ’ 300 ์ƒํ–ฅ (์ด์ „ ์„ธ์…˜์—์„œ ์ง„ํ–‰) - -- `DEFAULT_MAX_ITERATIONS = 300` (์ด์ „ 100 โ†’ 300, ์ด๋ฏธ 08-21 ์„ธ์…˜์—์„œ ๋ณ€๊ฒฝ๋จ) - - -## 2026-08-23 ์„ธ์…˜ (์ด์–ด์„œ): ๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ ๋ฒ„๊ทธ ์ˆ˜์ • - -์‹œ์ž‘์ : ์ด์ „ ์„ธ์…˜์˜ ๋””๋ฒ„๊ทธ ๋กœ๊ทธ ์ œ๊ฑฐ + ๋“œ๋ž˜๊ทธ ๋ฒ„๊ทธ ์ˆ˜์ •. - -### ๋””๋ฒ„๊ทธ ๋กœ๊ทธ ์ œ๊ฑฐ (`App.tsx`) - -- `process.stderr.write(\`[MOUSE] ...\`)` 2์ค„ ์‚ญ์ œ โ€” ์Šคํฌ๋กค ์‹œ ๋กœ๊ทธ๊ฐ€ ์Ÿ์•„์ง€๋Š” ์›์ธ - -### ๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ ๋ฒ„๊ทธ ์ˆ˜์ • (`mouseInput.ts`, `App.tsx`) - -**๋ฌธ์ œ**: ๋งˆ์šฐ์Šค ์™ผ์ชฝ ๋ฒ„ํŠผ ๋“œ๋ž˜๊ทธ๊ฐ€ ์ž‘๋™ํ•˜์ง€ ์•Š์Œ. ํด๋ฆญ์€ ๋˜์ง€๋งŒ ๋“œ๋ž˜๊ทธ๋กœ ์„ ํƒ ์˜์—ญ์„ ํ™•์žฅํ•  ์ˆ˜ ์—†์—ˆ์Œ. - -**์›์ธ**: SGR-1006 ํ”„๋กœํ† ์ฝœ์—์„œ **๋“œ๋ž˜๊ทธ ๋ชจ์…˜**์€ ๋ฒ„ํŠผ ์ฝ”๋“œ์— **bit 32**๋ฅผ ๋”ํ•ด ์ „์†ก: -- ์™ผ์ชฝ ๋ฒ„ํŠผ ํด๋ฆญ: `button=0` -- ์™ผ์ชฝ ๋ฒ„ํŠผ **๋“œ๋ž˜๊ทธ**: `button=32` (0 + motion bit 32) -- ๋ฆด๋ฆฌ์Šค: `button=3` + trailing `m` - -๊ธฐ์กด `logicalButton()`์€ `button & ~0x07`๋กœ ๋ชจ๋””ํŒŒ์ด์–ด ๋น„ํŠธ๋งŒ ์ œ๊ฑฐ โ†’ `32 & ~0x07 = 32` โ†’ `"other"`๋กœ ๋ถ„๋ฅ˜ โ†’ ๋“œ๋ž˜๊ทธ ์ด๋ฒคํŠธ๊ฐ€ ์™„์ „ํžˆ ๋ฌด์‹œ๋จ. - -์ด์ „ ์ˆ˜์ •์—์„œ `button=3`์„ `pressed ? "drag" : "release"`๋กœ ๋ฐ”๊ฟจ์œผ๋‚˜, `button=3`์€ ์‹ค์ œ๋กœ๋Š” 3๋ฒˆ ๋ฒ„ํŠผ ๋ˆ„๋ฆ„(๊ฑฐ์˜ ๋ฐœ์ƒ ์•ˆ ํ•จ)์ด๊ณ , SGR-1006 ๋ฆด๋ฆฌ์Šค๋Š” ํ•ญ์ƒ `pressed=false`(trailing `m`)์œผ๋กœ ์˜ค๋ฏ€๋กœ `button=3` ์—ฌ๋ถ€์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ `!pressed โ†’ "release"`๊ฐ€ ๋งž์Œ. - -**์ˆ˜์ •** (`mouseInput.ts`): -- `logicalButton()` ์žฌ์ž‘์„ฑ: - - `raw === 0 && pressed` โ†’ `"left"` (ํด๋ฆญ) - - `raw === 1 && pressed` โ†’ `"middle"` - - `raw === 2 && pressed` โ†’ `"right"` - - `raw === 32/33/34` โ†’ `"drag"` (์™ผ์ชฝ/๊ฐ€์šด๋ฐ/์˜ค๋ฅธ์ชฝ ๋“œ๋ž˜๊ทธ ๋ชจ์…˜) - - `raw === 64/65` โ†’ `"wheel-up"/"wheel-down"` - - `!pressed` โ†’ `"release"` (SGR-1006 ๋ฆด๋ฆฌ์Šค) - - ๋‚˜๋จธ์ง€ โ†’ `"other"` -- SGR-1006 ๋ฒ„ํŠผ ์ธ์ฝ”๋”ฉ ์ „์ฒด ๋ฌธ์„œํ™” (bit 0-2 ๋ชจ๋””ํŒŒ์ด์–ด, bit 4-5 ๋ฒ„ํŠผ, bit 5 ๋ชจ์…˜, bit 6 ํœ ) - -**์ˆ˜์ •** (`App.tsx`): -- ๋งˆ์šฐ์Šค ํ•ธ๋“ค๋Ÿฌ ๋ถ„๊ธฐ ์žฌ๊ตฌ์„ฑ: - - `"left" && pressed` โ†’ ์„ ํƒ ์‹œ์ž‘ - - `"drag" && selectionStart` โ†’ ์„ ํƒ ์˜์—ญ ํ™•์žฅ (๋“œ๋ž˜๊ทธ) - - `"release" && selectionStart` โ†’ ํด๋ฆฝ๋ณด๋“œ ๋ณต์‚ฌ + ์„ ํƒ ํ•ด์ œ - - ๊ธฐ์กด `left && !pressed` ๋ถ„๊ธฐ ์ œ๊ฑฐ (SGR-1006์—์„œ ๋ฆด๋ฆฌ์Šค๋Š” ํ•ญ์ƒ `button=3, pressed=false`) -- ํ…œํ”Œ๋ฆฟ ๋ฆฌํ„ฐ๋Ÿด ๋‚ด ` -` ์ด์Šค์ผ€์ดํ”„ ๊นจ์ง ์ˆ˜์ • (CRLF ํŒŒ์ผ ๋ฐ”์ด๋„ˆ๋ฆฌ ํŽธ์ง‘์œผ๋กœ ์ธํ•ด ๋ฆฌํ„ฐ๋Ÿด ๊ฐœํ–‰์ด ๋จ) - -### ๊ฒ€์ฆ -- `tsc --noEmit` โœ“ -- `npm run build` โœ“ (293.39 KB) -- `npm test` โœ“ 42ํŒŒ์ผ 326๊ฐœ ์ „๋ถ€ ํ†ต๊ณผ - ---- - -## 2026-08-22 ์„ธ์…˜ (Claude Code): ๋งˆ์šฐ์Šค ํฌ๋ž˜์‹œ, CRLF ๋งค์นญ ๋ฒ„๊ทธ 2๊ฑด, ๋ฒˆ์ธ๋ ˆ์ดํŠธ ์˜คํ•ด - -์‹œ์ž‘์ : ์œ„ ์„ธ์…˜์ด ๋‚ด๋ณด๋‚ธ ํŠธ๋žœ์Šคํฌ๋ฆฝํŠธ(`08-22T03-43-04-499Z.md`)๋ฅผ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์„œ๋ธŒ์—์ด์ „ํŠธ๋กœ ๋ฆฌ๋ทฐ. ๊ฒ€์ฆ: typecheck โœ“ ยท build 295.77 KB ยท **339 tests** ํ†ต๊ณผ (์‹œ์ž‘ 326 โ†’ +13). - -### ๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ ํฌ๋ž˜์‹œ ์ˆ˜์ • (`App.tsx`) - -**๋ฌธ์ œ**: ์œ„ ์„ธ์…˜์ด `mouseToContentRow` ์•ˆ์— `effectiveScrollTopRef.current`๋ฅผ ์ฐธ์กฐํ•˜๋„๋ก ๊ณ ์ณค์ง€๋งŒ, ๊ทธ ref๋ฅผ ์„ ์–ธํ•˜๋Š” ์ค„์„ ์ถ”๊ฐ€ํ•˜๊ธฐ ์ „์— ์„ธ์…˜์ด ๋Š๊น€. `npm run build`(esbuild)๋Š” ํƒ€์ž…์ฒดํฌ๋ฅผ ์•ˆ ํ•ด์„œ "๋นŒ๋“œ ์„ฑ๊ณต"์œผ๋กœ ๋ณด์˜€์ง€๋งŒ `tsc --noEmit`์€ `Cannot find name 'effectiveScrollTopRef'`๋กœ ์‹คํŒจ โ€” ์ฆ‰ ์‹ค์ œ๋กœ๋Š” ๋งˆ์šฐ์Šค๋ฅผ ํด๋ฆญํ•˜๋Š” ์ˆœ๊ฐ„ `ReferenceError`๋กœ ์ฃฝ๋Š” ์ƒํƒœ์˜€์Œ. - -**์ˆ˜์ •**: `maxScrollRef`/`pinnedToBottomRef`/`scrollTopRef`์™€ ๊ฐ™์€ ํŒจํ„ด์œผ๋กœ `effectiveScrollTopRef` ์„ ์–ธ + ๋งค ๋ Œ๋”๋งˆ๋‹ค `.current` ๊ฐฑ์‹  ํ•œ ์ค„ ์ถ”๊ฐ€. - -**๋‚จ์€ ๋ถˆํ™•์‹ค์„ฑ**: ์ขŒํ‘œ ๊ณ„์‚ฐ(`mouseToContentRow`) ์ž์ฒด๊ฐ€ ๋งž๋Š”์ง€๋Š” ์‹ค์ œ ํ„ฐ๋ฏธ๋„์—์„œ ๋“œ๋ž˜๊ทธํ•ด๋ณด์ง€ ์•Š์œผ๋ฉด ํ™•์‹  ๋ถˆ๊ฐ€ โ€” Yoga ๋ ˆ์ด์•„์›ƒ(`getAbsolutePosition`)์€ 0-based, SGR-1006 ๋งˆ์šฐ์Šค row๋Š” 1-based๋ผ 1์ค„ ์˜คํ”„๋ฐ”์ด์› ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์Œ. ์‚ฌ์šฉ์ž์—๊ฒŒ ์ตœ์ƒ๋‹จ ์ค„๋งŒ ๋“œ๋ž˜๊ทธํ•ด์„œ ๋ฌด์—‡์ด ๋ณต์‚ฌ๋˜๋Š”์ง€ ํ™•์ธ ์š”์ฒญํ•จ (์•„์ง ํšŒ์‹  ๋Œ€๊ธฐ). - -### CRLF/์ œ์–ด๋ฌธ์ž JSON ํŒŒ์‹ฑ ๋ฒ„๊ทธ (`src/toolcalling/partialJson.ts`, `nativeAdapter.ts`) - -JSON์€ ๋ฌธ์ž์—ด ๋ฆฌํ„ฐ๋Ÿด ์•ˆ์— raw ์ œ์–ด๋ฌธ์ž(๋ฆฌํ„ฐ๋Ÿด ๊ฐœํ–‰/CR/ํƒญ)๋ฅผ ๊ธˆ์ง€ โ€” ๋กœ์ปฌ ๋ชจ๋ธ์ด ๋ฉ€ํ‹ฐ๋ผ์ธ ํŒŒ์ผ ๋‚ด์šฉ์„ ์ด์Šค์ผ€์ดํ”„ ์—†์ด ๊ทธ๋Œ€๋กœ echoํ•˜๋ฉด(์ด ํ”„๋กœ์ ํŠธ ์†Œ์Šค ๋Œ€๋ถ€๋ถ„์ด CRLF๋ผ ํŠนํžˆ ํ”ํ•จ) `JSON.parse`๊ฐ€ "Bad control character" ๋กœ ํ†ต์งธ๋กœ ์‹คํŒจ. - -- `partialJson.ts`: `repairPartialJson`์— ์ƒˆ (2)๋‹จ๊ณ„๋กœ `escapeRawControlCharsInStrings` ์ถ”๊ฐ€ โ€” ๋ฌธ์ž์—ด ๋ฆฌํ„ฐ๋Ÿด ์•ˆ์˜ raw `\n`/`\r`/`\t`/๊ธฐํƒ€ ์ œ์–ด๋ฌธ์ž๋งŒ ์ด์Šค์ผ€์ดํ”„(๊ธฐ์กด `\\n` ๊ฐ™์€ ์ด์Šค์ผ€์ดํ”„๋Š” ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š์Œ), ๋‚˜๋จธ์ง€ ๋ฆฌํŽ˜์–ด ๋‹จ๊ณ„๋Š” ์ด ๊ฒฐ๊ณผ ์œ„์—์„œ ์‹คํ–‰. -- `nativeAdapter.ts`: `resolveToolCall`์˜ raw `JSON.parse`๊ฐ€ ์‹คํŒจํ•ด๋„ ๋ฆฌํŽ˜์–ด๋ฅผ ์ „ํ˜€ ์•ˆ ์‹œ๋„ํ•˜๋˜ ๊ฒƒ์„ `repairPartialJson` ํด๋ฐฑ ์ถ”๊ฐ€ (์ŠคํŠธ๋ฆฌ๋ฐ ๊ฒฝ๋กœ๋Š” `loop.ts`์— ์ด๋ฏธ ๋ฆฌํŽ˜์–ด๊ฐ€ ์žˆ์—ˆ์ง€๋งŒ ๋น„์ŠคํŠธ๋ฆฌ๋ฐ completion ๊ฒฝ๋กœ๋Š” ์—†์—ˆ์Œ). -- ํ…Œ์ŠคํŠธ: `partialJson.test.ts` +4, `nativeAdapter.test.ts` ์‹ ๊ทœ 3๊ฐœ. - -### CRLF `edit_file`/`multi_edit` ๋งค์นญ ๋ฒ„๊ทธ (`src/tools/editFile.ts`, `multiEdit.ts`) - -**์›์ธ**: `read_file`์€ ํ•ญ์ƒ LF๋กœ ์ •๊ทœํ™”ํ•œ ๋‚ด์šฉ์„ ๋ชจ๋ธ์—๊ฒŒ ๋ณด์—ฌ์ฃผ๋Š”๋ฐ(`content.split(/\r?\n/)` ํ›„ `\n`์œผ๋กœ ์žฌ์กฐํ•ฉ), `edit_file`/`multi_edit`์€ raw(์‹ค์ œ CRLF ๋ณด์กด) ํŒŒ์ผ๊ณผ ์ •ํ™•ํžˆ ๋งค์นญ โ€” ๋ชจ๋ธ์€ ์ž๊ธฐ๊ฐ€ ๋ณธ LF ๊ธฐ์ค€์œผ๋กœ `old_string`์„ ๋งŒ๋“œ๋‹ˆ CRLF ํŒŒ์ผ๋งˆ๋‹ค "old_string not found"๊ฐ€ ์ฒด๊ณ„์ ์œผ๋กœ ๋ฐœ์ƒํ•˜๋˜ ์ƒํƒœ. - -**์ˆ˜์ •**: `editFile.ts`์— `detectEol`/`toLF`/`fromLF` ์ถ”๊ฐ€ โ€” ํŒŒ์ผ์˜ EOL์„ ๊ฐ์ง€ํ•˜๊ณ , ๋งค์นญ/ํŽธ์ง‘์€ ํ•ญ์ƒ LF ๊ณต๊ฐ„์—์„œ ์ˆ˜ํ–‰, ์“ฐ๊ธฐ ์ง์ „์— ์›๋ž˜ EOL๋กœ ๋ณต์›. `multiEdit.ts`๋Š” ์ด ํ—ฌํผ๋“ค์„ ์žฌ์‚ฌ์šฉ(์ž์ฒด raw ๋งค์นญ ๋กœ์ง ์žˆ์—ˆ์Œ โ€” ๋™์ผ ๋ฒ„๊ทธ). -- ํ…Œ์ŠคํŠธ: `editFile.test.ts` +5(EOL ํ—ฌํผ 2๊ฐœ ํฌํ•จ), `multiEdit.test.ts` +1. - -### ๋ฒˆ์ธ๋ ˆ์ดํŠธ(๐Ÿ”ฅ) ์˜คํ•ด ์†Œ์ง€ ์ˆ˜์ • (`StatusBar.tsx`) - -`๐Ÿ”ฅ X/min`์ด `(inputTokens + outputTokens) / ๊ฒฝ๊ณผ๋ถ„`์ด์—ˆ๋Š”๋ฐ, `inputTokens`๋Š” API ํ˜ธ์ถœ๋งˆ๋‹ค **๋งค๋ฒˆ ์žฌ์ „์†ก๋˜๋Š” ์ „์ฒด ์ปจํ…์ŠคํŠธ**์˜ ํ•ฉ(ํ”„๋กฌํ”„ํŠธ ์บ์‹ฑ ์—†์Œ) โ€” ํด๋ฐฑ ๋ชจ๋“œ์ฒ˜๋Ÿผ ํ•œ ํ„ด์— ํˆด์ฝœ ์™•๋ณต์ด ๋งŽ์œผ๋ฉด ๋ช‡ ๋ถ„ ๋งŒ์— ์ˆ˜๋ฐฑK๋กœ ๋ถ€ํ’€์–ด ๋ณด์ž„(์‹ค์ œ ์ƒ์„ฑ ์†๋„์™€ ๋ฌด๊ด€). `outputTokens / ๊ฒฝ๊ณผ๋ถ„`์œผ๋กœ ๋ณ€๊ฒฝ โ€” ์‹ค์ œ ์ƒ์„ฑ ์ฒ˜๋ฆฌ๋Ÿ‰๋งŒ ๋ฐ˜์˜. ์ž…๋ ฅ์ธก ๋น„์šฉ์€ ์ƒ๋‹จ ์ปจํ…์ŠคํŠธ ๋ฐ”๊ฐ€ ์ด๋ฏธ ๋ณด์—ฌ์ฃผ๊ณ  ์žˆ์–ด ์ •๋ณด ์†์‹ค ์—†์Œ. - -### ๊ฒ€์ฆ -- `tsc --noEmit` โœ“ -- `npm run build` โœ“ (295.77 KB) -- `npm test` โœ“ 43ํŒŒ์ผ **339๊ฐœ** ์ „๋ถ€ ํ†ต๊ณผ +- ์˜์กด์„ฑ ์—…๊ทธ๋ ˆ์ด๋“œ ์ปค๋ฐ‹ (working tree์— ๋ฏธ๋ฐ˜์˜ ๋ณ€๊ฒฝ ๋‹ค์ˆ˜) +- Normal screen ์•„ํ‚คํ…์ฒ˜ ์ „ํ™˜ ์ปค๋ฐ‹ (mouseInput.ts ์‚ญ์ œ, index.tsx/App.tsx ์žฌ๊ตฌ์กฐํ™”) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e11794d..a9f5188 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,22 +8,22 @@ "name": "locode", "version": "0.5.2", "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/sdk": "^1.30.0", "@vscode/ripgrep": "^1.18.0", - "commander": "^13.0.0", + "commander": "^15.0.0", "diff": "^9.0.0", "env-paths": "^4.0.0", - "execa": "^9.6.1", + "execa": "^10.0.1", "fast-glob": "^3.3.3", - "ink": "^7.1.0", + "ink": "^7.1.1", "ink-select-input": "^6.2.0", "ink-spinner": "^5.0.0", "ink-text-input": "^6.0.0", "marked": "^15.0.12", "marked-terminal": "^7.3.0", - "openai": "^6.45.0", - "react": "^19.2.7", - "string-width": "^8.2.1", + "openai": "^7.5.0", + "react": "^19.2.8", + "string-width": "^8.2.2", "tree-kill": "^1.2.2", "vscode-languageserver-protocol": "^3.18.2", "vscode-uri": "^3.1.0", @@ -34,12 +34,12 @@ }, "devDependencies": { "@types/marked-terminal": "^6.1.1", - "@types/node": "^22.10.0", - "@types/react": "^19.2.17", + "@types/node": "^26.2.0", + "@types/react": "^19.2.18", "tsup": "^8.3.0", - "tsx": "^4.19.0", + "tsx": "^4.23.12", "typescript": "^5.7.0", - "vitest": "^3.0.0" + "vitest": "^4.1.11" }, "engines": { "node": ">=20" @@ -589,12 +589,12 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", - "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.9", + "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", @@ -663,6 +663,296 @@ "node": ">= 8" } }, + "node_modules/@oxc-project/types": { + "version": "0.146.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.146.0.tgz", + "integrity": "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz", + "integrity": "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.5.tgz", + "integrity": "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz", + "integrity": "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.5.tgz", + "integrity": "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.5.tgz", + "integrity": "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.5.tgz", + "integrity": "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.5.tgz", + "integrity": "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.5.tgz", + "integrity": "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.5.tgz", + "integrity": "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.5.tgz", + "integrity": "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.5.tgz", + "integrity": "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.5.tgz", + "integrity": "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.5.tgz", + "integrity": "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.5.tgz", + "integrity": "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.5.tgz", + "integrity": "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.62.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", @@ -1043,6 +1333,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/cardinal": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@types/cardinal/-/cardinal-2.1.1.tgz", @@ -1102,19 +1399,19 @@ } }, "node_modules/@types/node": { - "version": "22.20.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", - "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~8.3.0" } }, "node_modules/@types/react": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", - "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", "devOptional": true, "license": "MIT", "dependencies": { @@ -1122,39 +1419,40 @@ } }, "node_modules/@vitest/expect": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz", - "integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", "dev": true, "license": "MIT", "dependencies": { + "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.6", - "@vitest/utils": "3.2.6", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz", - "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.6", + "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" + "magic-string": "^0.30.21" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -1166,42 +1464,42 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", - "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^2.0.0" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz", - "integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.6", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" + "@vitest/utils": "4.1.11", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz", - "integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.6", - "magic-string": "^0.30.17", + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", "pathe": "^2.0.3" }, "funding": { @@ -1209,28 +1507,25 @@ } }, "node_modules/@vitest/spy": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz", - "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz", - "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.6", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" + "@vitest/pretty-format": "4.1.11", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -1655,18 +1950,11 @@ } }, "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, "engines": { "node": ">=18" } @@ -1692,16 +1980,6 @@ "node": ">=10" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -1898,12 +2176,12 @@ "license": "MIT" }, "node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", "license": "MIT", "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/confbox": { @@ -1945,6 +2223,13 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/convert-to-spaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", @@ -2027,16 +2312,6 @@ } } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -2046,6 +2321,16 @@ "node": ">= 0.8" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/diff": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", @@ -2142,9 +2427,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", "dev": true, "license": "MIT" }, @@ -2277,26 +2562,26 @@ } }, "node_modules/execa": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", - "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-10.0.1.tgz", + "integrity": "sha512-ge98qjkRK4IB7tL7Ju/6qmm5LHoH1eEMt5FNZrz3f4UIYhF28lggX20z3FaX1sgc67msLEn0N0BscOs29iuwyw==", "license": "MIT", "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", - "cross-spawn": "^7.0.6", "figures": "^6.1.0", - "get-stream": "^9.0.0", + "get-stream": "^9.0.1", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", - "pretty-ms": "^9.2.0", + "pretty-ms": "^9.3.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", - "yoctocolors": "^2.1.1" + "which-command": "^0.1.0", + "yoctocolors": "^2.1.2" }, "engines": { - "node": "^18.19.0 || >=20.5.0" + "node": ">=22" }, "funding": { "url": "https://github.com/sindresorhus/execa?sponsor=1" @@ -2753,9 +3038,9 @@ "license": "ISC" }, "node_modules/ink": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/ink/-/ink-7.1.0.tgz", - "integrity": "sha512-VWE6/yeLtFCJBNLflyI2OSylyXK1Rc24LuXup8Qt+icwkmmycFNdbn8IkSp6Frc0h1iA0NOvvi1ajW44U/w3Qg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/ink/-/ink-7.1.1.tgz", + "integrity": "sha512-Y43xxa1ZSPvpmfLHcN5o+OdP8Rf8ykkNJEuKYOUNZKT8wXVNLFTtEm1nSDMQkfBH+YANF4Xuu0hhZ4ejqAtN2w==", "license": "MIT", "dependencies": { "@alcalzone/ansi-tokenize": "^0.3.0", @@ -3137,13 +3422,6 @@ "node": ">=10" } }, - "node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -3156,6 +3434,279 @@ "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", "license": "BSD-2-Clause" }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -3186,13 +3737,6 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -3456,6 +4000,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -3478,10 +4036,13 @@ } }, "node_modules/openai": { - "version": "6.45.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.45.0.tgz", - "integrity": "sha512-5DQVNErssk0afNpTTHUm/qZPU4iKR9OYdNid8Ib4puq4gHNNvGWZht2zY4h9a8JMF949Ik6m8gQutllVPbjdnw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-7.5.0.tgz", + "integrity": "sha512-ZbDBz8FSB8Mv8fFYIUvzTFMdV5vl93/octp1MdtK2lfYepSpfv/ewmeugpKz/cwGtFSx+YuUM4NwpZ2P55YiPA==", "license": "Apache-2.0", + "engines": { + "node": ">=22.0.0" + }, "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", @@ -3584,16 +4145,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3810,9 +4361,9 @@ } }, "node_modules/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", - "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3885,6 +4436,40 @@ "node": ">=0.10.0" } }, + "node_modules/rolldown": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.5.tgz", + "integrity": "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.146.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.5", + "@rolldown/binding-android-arm64": "1.2.5", + "@rolldown/binding-darwin-arm64": "1.2.5", + "@rolldown/binding-darwin-x64": "1.2.5", + "@rolldown/binding-freebsd-x64": "1.2.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", + "@rolldown/binding-linux-arm64-gnu": "1.2.5", + "@rolldown/binding-linux-arm64-musl": "1.2.5", + "@rolldown/binding-linux-ppc64-gnu": "1.2.5", + "@rolldown/binding-linux-s390x-gnu": "1.2.5", + "@rolldown/binding-linux-x64-gnu": "1.2.5", + "@rolldown/binding-linux-x64-musl": "1.2.5", + "@rolldown/binding-openharmony-arm64": "1.2.5", + "@rolldown/binding-win32-arm64-msvc": "1.2.5", + "@rolldown/binding-win32-x64-msvc": "1.2.5" + } + }, "node_modules/rollup": { "version": "4.62.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", @@ -4248,16 +4833,16 @@ } }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", "dev": true, "license": "MIT" }, "node_modules/string-width": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", - "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", "license": "MIT", "dependencies": { "get-east-asian-width": "^1.5.0", @@ -4297,19 +4882,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -4447,30 +5019,10 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", - "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", "dev": true, "license": "MIT", "engines": { @@ -4580,9 +5132,9 @@ } }, "node_modules/tsx": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.5.tgz", - "integrity": "sha512-F7JnSfPl5ASt6LqwWyUQ3T8BwN3q0eQEbFMYa2iRWaVQmmudo0d7fRmwM4O002gsvW1bs0yBYioutsAjqLJMvQ==", + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", "dev": true, "license": "MIT", "dependencies": { @@ -5147,9 +5699,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "dev": true, "license": "MIT" }, @@ -5193,18 +5745,17 @@ } }, "node_modules/vite": { - "version": "7.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", - "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0 || ^0.28.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -5220,9 +5771,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -5235,15 +5787,18 @@ "@types/node": { "optional": true }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, "jiti": { "optional": true }, "less": { "optional": true }, - "lightningcss": { - "optional": true - }, "sass": { "optional": true }, @@ -5267,89 +5822,80 @@ } } }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/vitest": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz", - "integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", "dev": true, "license": "MIT", "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.6", - "@vitest/mocker": "3.2.6", - "@vitest/pretty-format": "^3.2.6", - "@vitest/runner": "3.2.6", - "@vitest/snapshot": "3.2.6", - "@vitest/spy": "3.2.6", - "@vitest/utils": "3.2.6", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.6", - "@vitest/ui": "3.2.6", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, - "@types/debug": { + "@opentelemetry/api": { "optional": true }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { "optional": true }, "@vitest/ui": { @@ -5360,9 +5906,22 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/vscode-jsonrpc": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.1.tgz", @@ -5409,6 +5968,21 @@ "node": ">= 8" } }, + "node_modules/which-command": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/which-command/-/which-command-0.1.0.tgz", + "integrity": "sha512-XZyoF5/5hZtXitIwzrU4NKK+Wtbb9aB9CezUEw2Q0wlYK8NUYQxC1rRXgNueYLtBAJwXIb+/tFVk4dozciNJMA==", + "license": "MIT", + "bin": { + "which-command": "cli.js" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sindresorhus/which-command?sponsor=1" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", diff --git a/package.json b/package.json index deb4a4c..383ef01 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "dist" ], "engines": { - "node": ">=20" + "node": ">=22.12.0" }, "scripts": { "build": "tsup", @@ -20,22 +20,22 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/sdk": "^1.30.0", "@vscode/ripgrep": "^1.18.0", - "commander": "^13.0.0", + "commander": "^15.0.0", "diff": "^9.0.0", "env-paths": "^4.0.0", - "execa": "^9.6.1", + "execa": "^10.0.1", "fast-glob": "^3.3.3", - "ink": "^7.1.0", + "ink": "^7.1.1", "ink-select-input": "^6.2.0", "ink-spinner": "^5.0.0", "ink-text-input": "^6.0.0", "marked": "^15.0.12", "marked-terminal": "^7.3.0", - "openai": "^6.45.0", - "react": "^19.2.7", - "string-width": "^8.2.1", + "openai": "^7.5.0", + "react": "^19.2.8", + "string-width": "^8.2.2", "tree-kill": "^1.2.2", "vscode-languageserver-protocol": "^3.18.2", "vscode-uri": "^3.1.0", @@ -43,12 +43,12 @@ }, "devDependencies": { "@types/marked-terminal": "^6.1.1", - "@types/node": "^22.10.0", - "@types/react": "^19.2.17", + "@types/node": "^26.2.0", + "@types/react": "^19.2.18", "tsup": "^8.3.0", - "tsx": "^4.19.0", + "tsx": "^4.23.12", "typescript": "^5.7.0", - "vitest": "^3.0.0" + "vitest": "^4.1.11" }, "allowScripts": { "esbuild@0.27.2": true diff --git a/src/agent/loop.test.ts b/src/agent/loop.test.ts index 2227fc2..c460321 100644 --- a/src/agent/loop.test.ts +++ b/src/agent/loop.test.ts @@ -1,13 +1,44 @@ import { describe, expect, it, vi } from "vitest"; import { z } from "zod"; -import { MaxIterationsError, compactSession, runTurn, shouldAutoCompact } from "./loop.js"; +import { AgentError, MaxIterationsError, compactSession, detectRepetitionLoop, runTurn, shouldAutoCompact } from "./loop.js"; import { agentTool } from "../tools/agentTool.js"; import { createSession } from "./session.js"; import { buildToolSet } from "../tools/toolset.js"; +import { DEFAULT_MAX_OUTPUT_TOKENS } from "../config/defaults.js"; import type { ConfirmFn } from "../permissions/types.js"; import type { ToolDef } from "../tools/types.js"; import type { Session } from "./session.js"; +describe("detectRepetitionLoop", () => { + it("returns false for short text regardless of content", () => { + expect(detectRepetitionLoop("error error error error error error")).toBe(false); + }); + + it("detects a short phrase repeating many times in a row", () => { + const text = "Here is the analysis: " + "error error error ".repeat(80); + expect(detectRepetitionLoop(text)).toBe(true); + }); + + it("detects a longer repeated unit (e.g. a repeated JSON-like fragment)", () => { + const unit = '{"status":"retry","reason":"pending"} '; + const text = "Starting work.\n" + unit.repeat(40); + expect(detectRepetitionLoop(text)).toBe(true); + }); + + it("does not flag normal varied prose", () => { + const paragraphs = Array.from( + { length: 20 }, + (_, i) => `Paragraph ${i}: this covers a different point each time, with enough unique wording to avoid any short repeating pattern in the tail.`, + ); + expect(detectRepetitionLoop(paragraphs.join("\n"))).toBe(false); + }); + + it("does not flag a legitimately repetitive but non-degenerate bullet list", () => { + const items = Array.from({ length: 30 }, (_, i) => `- item ${i}: some unique detail about entry number ${i}`); + expect(detectRepetitionLoop(items.join("\n"))).toBe(false); + }); +}); + describe("shouldAutoCompact", () => { it("triggers at the session's configured threshold", () => { const session = { @@ -25,8 +56,8 @@ describe("shouldAutoCompact", () => { describe("runTurn / max_tokens", () => { it("caps max_tokens independent of a large contextWindow", async () => { // Regression: some backends advertise a huge context window but cap a single response's - // max_tokens far below it (e.g. Ollama's glm-5.2:cloud: 1,000,000-token context, 8192-token max - // output). resolveMaxTokens used to request up to the whole remaining window, which such + // max_tokens far below it (e.g. Ollama's glm-5.2:cloud: 1,000,000-token context, 131,072-token + // max output). resolveMaxTokens used to request up to the whole remaining window, which such // backends reject outright โ€” worse the larger (or user-raised) contextWindow got. let capturedMaxTokens: number | undefined; const fakeClient = { @@ -55,7 +86,38 @@ describe("runTurn / max_tokens", () => { await runTurn(session, "hi", () => {}); - expect(capturedMaxTokens).toBeLessThanOrEqual(8192); + expect(capturedMaxTokens).toBeLessThanOrEqual(DEFAULT_MAX_OUTPUT_TOKENS); + }); +}); + +describe("runTurn / repetition detection", () => { + it("aborts a streamed response that degenerates into a repetition loop", async () => { + const fakeClient = { + chat: { + completions: { + create: vi.fn(async (_params: any, options: any) => { + const signal: AbortSignal | undefined = options?.signal; + let count = 0; + // A real backend stuck in a loop would keep streaming the same short phrase forever; + // this fake mirrors that but stops once aborted (or hits a safety cap, so a detection + // regression fails the test instead of hanging it). + return { + [Symbol.asyncIterator]: () => ({ + next: async () => { + if (signal?.aborted || count >= 500) return { done: true, value: undefined }; + count++; + return { done: false, value: { choices: [{ delta: { content: "loop " }, finish_reason: null }] } }; + }, + }), + }; + }), + }, + }, + } as any; + + const session = createSession(fakeClient, "test-model", process.cwd(), async () => "once", "native", []); + + await expect(runTurn(session, "hi", () => {})).rejects.toThrow(AgentError); }); }); diff --git a/src/agent/loop.ts b/src/agent/loop.ts index 17a797b..b063174 100644 --- a/src/agent/loop.ts +++ b/src/agent/loop.ts @@ -123,6 +123,37 @@ export function createIdleAbort( }; } +/** Detects a degenerate repetition loop: a short substring repeating many times in a row at the + * tail of the streamed text. Known failure mode for local/quantized models, which otherwise only + * stop at max_tokens or an idle timeout โ€” with maxOutputTokens raised for backends whose real + * ceiling is far larger, an undetected loop could run for a very long time before either kicks in. + * Only checks the tail (loops are contiguous, so recent text is sufficient and cheap to scan) and + * requires several consecutive exact repeats, so legitimately repetitive-but-valid text (a bullet + * list, a table, ASCII art) doesn't false-positive. */ +export function detectRepetitionLoop(text: string): boolean { + const TAIL = 1000; + const MIN_PERIOD = 4; + const MAX_PERIOD = 200; + const MIN_REPEATS = 6; + if (text.length < TAIL) return false; + const tail = text.slice(-TAIL); + for (let period = MIN_PERIOD; period <= MAX_PERIOD; period++) { + const repeats = Math.floor(tail.length / period); + if (repeats < MIN_REPEATS) continue; + const unit = tail.slice(tail.length - period); + let matched = true; + for (let r = 2; r <= repeats; r++) { + const start = tail.length - r * period; + if (tail.slice(start, start + period) !== unit) { + matched = false; + break; + } + } + if (matched) return true; + } + return false; +} + const MAX_MALFORMED_RETRIES = 2; /** Local models (especially small quantized ones) occasionally emit a bare empty `stop` * chunk โ€” no text, no tool calls. Rather than abort the whole turn as a hard error, retry @@ -949,6 +980,12 @@ export async function runTurn( const requestStart = Date.now(); const idleGuard = createIdleAbort(resolveRequestTimeoutMs(), signal); + // Independent from idleGuard โ€” a repetition loop keeps producing chunks, so idleGuard's + // silence-based timer never trips. Combined below via AbortSignal.any so either guard can end + // the request; only this one needs its own controller since it fires from content, not silence. + const repetitionAbort = new AbortController(); + let repetitionDetected = false; + let lastRepetitionCheckLength = 0; try { const stream = await session.client.chat.completions.create( { @@ -959,7 +996,7 @@ export async function runTurn( stream_options: { include_usage: true }, max_tokens: resolveMaxTokens(session), }, - { signal: idleGuard.signal }, + { signal: AbortSignal.any([idleGuard.signal, repetitionAbort.signal]) }, ); for await (const chunk of stream) { @@ -986,6 +1023,15 @@ export async function runTurn( if (delta?.content) { fullText += delta.content; emit({ type: "text_delta", delta: delta.content }); + // Throttled to roughly every 100 new characters โ€” detectRepetitionLoop is cheap per call, + // but there's no reason to run it on every single-token chunk. + if (fullText.length - lastRepetitionCheckLength >= 100) { + lastRepetitionCheckLength = fullText.length; + if (detectRepetitionLoop(fullText)) { + repetitionDetected = true; + repetitionAbort.abort(); + } + } } // Accumulate native tool call deltas @@ -1015,6 +1061,11 @@ export async function runTurn( if (fullText) { emit({ type: "text_done", fullText }); } + if (repetitionDetected) { + throw new AgentError( + "Detected a repetition loop in the model's response and aborted early โ€” the model got stuck repeating itself. Try again; if it keeps happening, this model may need a different prompt or a lower temperature.", + ); + } if (idleGuard.didTimeOut()) { throw new AgentError( `Backend stopped responding mid-stream (no data for ${Math.round(resolveRequestTimeoutMs() / 1000)}s) โ€” connection aborted. The backend may have crashed or hung; try again.`, @@ -1025,6 +1076,15 @@ export async function runTurn( idleGuard.dispose(); } + // A repetition-triggered abort can also surface as a clean (chunk-less) end of the async + // iterator instead of a thrown error, depending on how far into the stream it landed โ€” check + // unconditionally rather than only in the catch above, mirroring the idle-timeout check below. + if (repetitionDetected) { + throw new AgentError( + "Detected a repetition loop in the model's response and aborted early โ€” the model got stuck repeating itself. Try again; if it keeps happening, this model may need a different prompt or a lower temperature.", + ); + } + // An idle-triggered abort can also surface as a clean (chunk-less) end of the async iterator // instead of a thrown error, depending on how far into the stream it landed โ€” check unconditionally // rather than only in the catch above, or it'd silently fall through to "Empty response from model." diff --git a/src/codeintel/lspManager.ts b/src/codeintel/lspManager.ts index 5ce015d..24df29e 100644 --- a/src/codeintel/lspManager.ts +++ b/src/codeintel/lspManager.ts @@ -150,12 +150,35 @@ function toLocation(loc: Location): LocResult { async function startServer(spec: LanguageSpec, cwd: string): Promise { let child: ChildProcess; try { - child = spawn(spec.command, spec.args ?? [], { cwd, stdio: ["pipe", "pipe", "pipe"] }); + // npm installs global CLI packages on Windows as .cmd/.ps1 shims, not raw .exe files โ€” spawn() + // can't resolve those without shell:true, so a genuinely-installed server would otherwise ENOENT. + child = spawn(spec.command, spec.args ?? [], { + cwd, + stdio: ["pipe", "pipe", "pipe"], + shell: process.platform === "win32", + }); } catch (err) { throw new Error( `Could not start the LSP server "${spec.command}" for ${spec.languageId}. Is it installed and on your PATH? (${(err as Error).message})`, ); } + // spawn() itself rarely throws synchronously โ€” a missing binary (ENOENT) instead fires an + // async 'error' event on the child process. With no listener, that event is unhandled and + // crashes the whole process, so wait for either a successful spawn or that error before + // proceeding. + await new Promise((resolve, reject) => { + child.once("spawn", () => resolve()); + child.once("error", (err) => { + reject( + new Error( + `Could not start the LSP server "${spec.command}" for ${spec.languageId}. Is it installed and on your PATH? (${(err as Error).message})`, + ), + ); + }); + }); + // After startup, a late 'error' (e.g. the process dying unexpectedly) must not go unhandled + // either โ€” the 'exit' handler below already disposes the connection, so just swallow it here. + child.on("error", () => {}); if (!child.stdin || !child.stdout) { child.kill(); throw new Error(`LSP server "${spec.command}" did not open stdio streams.`); @@ -164,6 +187,9 @@ async function startServer(spec: LanguageSpec, cwd: string): Promise const reader = new StreamMessageReader(child.stdout); const writer = new StreamMessageWriter(child.stdin); const connection = createProtocolConnection(reader, writer); + // vscode-jsonrpc buffers all messages until listen() starts pumping them โ€” without this, + // sendRequest hangs (nothing is ever written) or throws "Call listen() first." + connection.listen(); // Surface stderr so a crashing server isn't a silent void (matches locode's MCP stdio policy). child.stderr?.on("data", () => { diff --git a/src/config/defaults.ts b/src/config/defaults.ts index d1aa7fc..c906141 100644 --- a/src/config/defaults.ts +++ b/src/config/defaults.ts @@ -26,12 +26,17 @@ export const DEFAULT_CONTEXT_WINDOW = 8192; /** Ceiling on a single response's `max_tokens`, independent of the model's context window. Most * backends cap how much a single completion can generate well below the total context window they - * advertise (e.g. Ollama's glm-5.2:cloud reports a 1,000,000-token context window but only ever - * generates up to 8192 tokens per response) โ€” resolveMaxTokens (agent/loop.ts) used to request up - * to the whole remaining window, which such backends rejected outright as a context/length error - * even on the very first turn. 8192 is a safe default most backends support; raise it via - * `locode config set maxOutputTokens` for backends known to allow more. */ -export const DEFAULT_MAX_OUTPUT_TOKENS = 8192; + * advertise (e.g. Ollama's glm-5.1:cloud / glm-5.2:cloud report a 1,000,000-token context window + * but error on a request above their real 131,072-token output cap: "max_tokens (500000) exceeds + * model's maximum output tokens (131072)") โ€” resolveMaxTokens (agent/loop.ts) used to request up to + * the whole remaining window, which such backends rejected outright as a context/length error even + * on the very first turn. 131072 (128K) matches that verified real ceiling exactly, so it's safe to + * use as the default without erroring on the very first turn โ€” going straight to a backend's real + * cap only became reasonable once detectRepetitionLoop (agent/loop.ts) existed to abort a model + * that gets stuck generating instead of relying on this value alone as the safety valve. Raise it + * further via `locode config set maxOutputTokens` for backends known to allow more; lower it for + * ones with a smaller real ceiling. */ +export const DEFAULT_MAX_OUTPUT_TOKENS = 131_072; /** Max model requests per turn before locode pauses rather than looping forever. Each iteration * is one model generation request (one tool-call round-trip), and local models commonly issue a diff --git a/src/config/store.ts b/src/config/store.ts index 4e0d798..19dc759 100644 --- a/src/config/store.ts +++ b/src/config/store.ts @@ -44,20 +44,36 @@ export function configFilePath(): string { /** @internal For tests only โ€” redirect config persistence to `path` (pass undefined to reset). */ export function _setConfigFilePathForTest(p: string | undefined): void { configFileOverride = p; + invalidateConfigCache(); } +// In-memory cache so every resolve*() call in the same process doesn't re-read and re-parse +// the same small JSON file (8โ€“10 calls during startup alone). Invalidated by saveStoredConfig +// and by tests that change the config path. +let cachedConfig: StoredConfig | undefined; + export function loadStoredConfig(): StoredConfig { + if (cachedConfig !== undefined) return cachedConfig; const file = configFilePath(); - if (!existsSync(file)) return {}; + if (!existsSync(file)) { cachedConfig = {}; return cachedConfig; } try { - return JSON.parse(readFileSync(file, "utf-8")) as StoredConfig; + cachedConfig = JSON.parse(readFileSync(file, "utf-8")) as StoredConfig; + return cachedConfig; } catch { - return {}; + cachedConfig = {}; + return cachedConfig; } } +/** Invalidate the in-memory cache โ€” called by saveStoredConfig and by tests that change + * the config file path. */ +export function invalidateConfigCache(): void { + cachedConfig = undefined; +} + export function saveStoredConfig(cfg: StoredConfig): void { const file = configFilePath(); mkdirSync(path.dirname(file), { recursive: true }); writeFileSync(file, JSON.stringify(cfg, null, 2)); + cachedConfig = cfg; } \ No newline at end of file diff --git a/src/persistence/sessionStore.test.ts b/src/persistence/sessionStore.test.ts index f49fa04..143b090 100644 --- a/src/persistence/sessionStore.test.ts +++ b/src/persistence/sessionStore.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs"; import path from "node:path"; import envPaths from "env-paths"; import { @@ -70,6 +70,19 @@ describe("sessionStore", () => { expect(listSessions()).toHaveLength(0); }); + it("rebuilds the summary index from disk when no index file exists yet", () => { + writeFileSync(path.join(dir, "manual-session.json"), JSON.stringify(makeRecord("manual-session"))); + expect(listSessions()).toHaveLength(1); + expect(listSessions()[0]?.id).toBe("manual-session"); + }); + + it("self-heals when a session file is deleted outside of deleteSession()", async () => { + await saveSession(makeRecord("will-vanish")); + expect(listSessions()).toHaveLength(1); + unlinkSync(path.join(dir, "will-vanish.json")); + expect(listSessions()).toHaveLength(0); + }); + it("deriveTitle extracts the first user message", () => { const title = deriveTitle([ { role: "system", content: "sys" }, diff --git a/src/persistence/sessionStore.ts b/src/persistence/sessionStore.ts index b452890..0f88a8e 100644 --- a/src/persistence/sessionStore.ts +++ b/src/persistence/sessionStore.ts @@ -1,5 +1,5 @@ import envPaths from "env-paths"; -import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs"; +import { existsSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs"; import path from "node:path"; import type { ChatCompletionMessageParam } from "openai/resources/chat/completions"; import type { ToolCallMode } from "../backend/capabilityProbe.js"; @@ -47,6 +47,86 @@ function filePath(id: string): string { return path.join(dir, `${safeSessionId(id)}.json`); } +const INDEX_FILENAME = "_index.json"; + +function indexFilePath(): string { + return path.join(dir, INDEX_FILENAME); +} + +function summarize(record: SessionRecord): SessionSummary { + return { + id: record.id, + updatedAt: record.updatedAt, + title: deriveTitle(record.messages), + model: record.model, + baseURL: record.baseURL, + messageCount: record.messages.length, + }; +} + +/** Reads the on-disk summary index and reconciles it against the actual session files, so a stale or + * missing index self-heals instead of ever going wrong: entries whose file was deleted (by this or + * another locode process) are dropped, and files present on disk but missing from the index (a fresh + * install, a crash before the last index write, another process's save racing this one) are parsed + * individually. This keeps the common case to O(session count) stat calls instead of O(total + * transcript bytes) โ€” listSessions() used to JSON.parse every saved session in full just to read 6 + * summary fields off each one. */ +function readIndex(): Map { + let index = new Map(); + if (existsSync(indexFilePath())) { + try { + const entries = JSON.parse(readFileSync(indexFilePath(), "utf-8")) as SessionSummary[]; + index = new Map(entries.map((e) => [e.id, e])); + } catch { + index = new Map(); + } + } + for (const id of index.keys()) { + if (!existsSync(filePath(id))) index.delete(id); + } + const known = new Set([...index.keys()].map((id) => safeSessionId(id))); + if (existsSync(dir)) { + for (const entry of readdirSync(dir)) { + if (!entry.endsWith(".json") || entry === INDEX_FILENAME) continue; + const stem = entry.slice(0, -".json".length); + if (known.has(stem)) continue; + try { + const record = JSON.parse(readFileSync(path.join(dir, entry), "utf-8")) as SessionRecord; + index.set(record.id, summarize(record)); + } catch { + // Skip corrupt/partial session files + } + } + } + return index; +} + +/** Best-effort atomic write of the index. A failed write just means the next readIndex() call + * re-parses whatever files it doesn't recognize yet โ€” never incorrect data, only a missed + * optimization. */ +function writeIndex(index: Map): void { + const tmp = `${indexFilePath()}.${process.pid}.${Date.now()}.tmp`; + try { + writeFileSync(tmp, JSON.stringify([...index.values()])); + renameSync(tmp, indexFilePath()); + } catch { + try { + unlinkSync(tmp); + } catch { + // tmp may not have been created if the write itself failed + } + } +} + +/** Updates one entry in the persisted index. Two saves for different sessions racing this can lose + * one's index write, but never lose data: readIndex() picks up any on-disk session file it doesn't + * recognize, so the loser just costs the next listSessions() call one extra parse. */ +function updateIndexEntry(record: SessionRecord): void { + const index = readIndex(); + index.set(record.id, summarize(record)); + writeIndex(index); +} + export function deriveTitle(messages: ChatCompletionMessageParam[]): string { const first = messages.find((m) => m.role === "user"); const text = first && typeof first.content === "string" ? first.content.trim() : ""; @@ -65,7 +145,8 @@ const saveQueues = new Map>(); * otherwise load as "no session found" (see loadSession, which treats an unparseable file as absent). */ export async function saveSession(record: SessionRecord): Promise { const file = filePath(record.id); - const write = () => writeFileAtomic(file, JSON.stringify(record, null, 2)); + const write = () => + writeFileAtomic(file, JSON.stringify(record, null, 2)).then(() => updateIndexEntry(record)); // Run whether or not the previous save rejected, so one failure can't stall the chain. const prev = saveQueues.get(record.id); const next = (prev ?? Promise.resolve()).then(write, write); @@ -94,24 +175,7 @@ export function loadSession(id: string): SessionRecord | undefined { export function listSessions(): SessionSummary[] { if (!existsSync(dir)) return []; - const summaries: SessionSummary[] = []; - for (const entry of readdirSync(dir)) { - if (!entry.endsWith(".json")) continue; - try { - const record = JSON.parse(readFileSync(path.join(dir, entry), "utf-8")) as SessionRecord; - summaries.push({ - id: record.id, - updatedAt: record.updatedAt, - title: deriveTitle(record.messages), - model: record.model, - baseURL: record.baseURL, - messageCount: record.messages.length, - }); - } catch { - // Skip corrupt/partial session files - } - } - return summaries.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)); + return [...readIndex().values()].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)); } export function mostRecentSessionId(): string | undefined { @@ -122,5 +186,8 @@ export function deleteSession(id: string): boolean { const file = filePath(id); if (!existsSync(file)) return false; unlinkSync(file); + const index = readIndex(); + index.delete(id); + writeIndex(index); return true; } diff --git a/src/ui/ink/App.tsx b/src/ui/ink/App.tsx index 1b2f89a..6c684e2 100644 --- a/src/ui/ink/App.tsx +++ b/src/ui/ink/App.tsx @@ -1,4 +1,4 @@ -import { Box, Text, useApp, useBoxMetrics, useInput, useStdin, useStdout, useWindowSize, type DOMElement } from "ink"; +import { Box, Static, Text, useApp, useInput, useStdin, useStdout, useWindowSize } from "ink"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import path from "node:path"; import { @@ -58,8 +58,6 @@ import { ExportPrompt } from "./ExportPrompt.js"; import { FilePanel, type FilePanelTab, type TouchedFile } from "./FilePanel.js"; import { HistoryItemView } from "./HistoryItemView.js"; import { ModelSelect } from "./ModelSelect.js"; -import { matchMouseSequence, logicalButton, copyToClipboard } from "./mouseInput.js"; -import { getAbsolutePosition } from "./absolutePosition.js"; import { PermissionPrompt } from "./PermissionPrompt.js"; import { SessionSelect } from "./SessionSelect.js"; import { StatusBar } from "./StatusBar.js"; @@ -67,77 +65,6 @@ import { ThinkingIndicator } from "./ThinkingIndicator.js"; import { ACCENT_HEX } from "../theme.js"; import { nextId, type HistoryItem, type NewHistoryItem } from "./types.js"; -/** Extract plain text lines from a HistoryItem (one line per display row for selection). */ -function itemToLines(item: HistoryItem): string[] { - switch (item.kind) { - case "user": return [`> ${item.text}`]; - case "assistant": return [item.text]; - case "thinking": return [item.text]; - case "streaming_text": return [item.text]; - case "tool_call": return [`โบ ${item.label}`]; - case "tool_result": return [` โŽฟ ${item.summary}`]; - case "notice": return [item.text]; - case "banner": - case "status": - case "dashboard": - case "help": - case "tools": - case "permissions": - case "sessions": - case "mcp": - case "plugins": - case "hooks": - case "skills": - case "todos": - // Complex items: we could render them fully, but for now return empty โ€” - // selection across these is rarely needed and their layout is complex. - return []; - } -} - -/** Given a row range (1-based content rows), extract the corresponding text from - * the history items + streaming text. Each item contributes its line(s); the result - * is the intersection of those lines with the [from.row..to.row] range. */ -function extractSelectionText( - items: HistoryItem[], - streamingText: string | null, - from: { row: number; col: number }, - to: { row: number; col: number }, -): string { - // Build a flat list of (lineNumber, text) pairs โ€” 1-based line numbers matching - // the content box rows (which scrollTop/marginTop offset against the viewport). - const lines: { row: number; text: string }[] = []; - let row = 1; - for (const item of items) { - const itemLines = itemToLines(item); - for (const line of itemLines) { - // A single item line may wrap to multiple terminal rows โ€” split on newlines - // (itemToLines already returns one string per logical line). - lines.push({ row, text: line }); - row++; - } - } - if (streamingText !== null) { - for (const line of streamingText.split("\n")) { - lines.push({ row, text: line }); - row++; - } - } - - const selected = lines.filter((l) => l.row >= from.row && l.row <= to.row); - if (selected.length === 0) return ""; - - return selected.map((l, i) => { - const isFirst = l.row === from.row; - const isLast = l.row === to.row; - let text = l.text; - if (isFirst) text = text.slice(from.col - 1); - if (isLast && selected.length > 1) text = text.slice(0, to.col - from.col + 1); // approximate - else if (isLast && selected.length === 1) text = text.slice(from.col - 1, to.col); - return text; - }).join("\n"); -} - // Cap for the input-history ring buffer used for โ†‘/โ†“ recall in the chat input. const MAX_HISTORY = 100; @@ -208,21 +135,6 @@ export function App({ const [inputValue, setInputValue] = useState(""); const [permission, setPermission] = useState(null); const [exportPrompt, setExportPrompt] = useState<{ defaultName: string; format: ExportFormat } | null>(null); - // Mouse tracking (xterm ?1002h button-event + ?1006h SGR format). On by default โ€” wheel - // scroll and click/drag events are captured for in-app interaction. Shift+click/drag is - // intentionally NOT captured so the terminal's native text selection still works (hold Shift - // to select and copy text). Click+drag selects text in-app (auto-copied on release). Toggle with `/mouse on|off`. When off, all mouse events pass - // through to the terminal and wheel scroll does nothing in-app. - const [mouseMode, setMouseMode] = useState(true); - // In-app text selection: track drag start/end rows (1-based terminal rows, adjusted - // for scroll offset). On release, the selected text is copied to the system clipboard - // via OSC 52 and the selection is cleared. - const [selectionStart, setSelectionStart] = useState<{ row: number; col: number } | null>(null); - const [selectionEnd, setSelectionEnd] = useState<{ row: number; col: number } | null>(null); - // Refs mirroring selection state so the useInput handler (which uses useEffectEvent) always - // reads the latest value without waiting for a React state flush cycle. - const selectionStartRef = useRef<{ row: number; col: number } | null>(null); - const selectionEndRef = useRef<{ row: number; col: number } | null>(null); const [streamingText, setStreamingText] = useState(null); const [isThinking, setIsThinking] = useState(false); const [permMode, setPermMode] = useState("default"); @@ -250,14 +162,6 @@ export function App({ // a pending permission prompt (via makeConfirmFn) โ€” the same abort plumbing sub-agent timeouts // already used, just wired up to a top-level turn for the first time. const turnAbortRef = useRef(null); - // Wraps whichever branch the bottom ternary renders (permission/export prompt, a picker, or the - // normal StatusBar+ChatInput column) โ€” measured (height only) so the history viewport above it - // knows exactly how much vertical space is left (the history viewport above it flexes). - const bottomSectionRef = useRef(null); - // Measures the history content's own natural (unclipped) height โ€” Yoga still computes a child's - // intrinsic size even when its parent has a fixed height + overflowY:hidden, so this reports the - // *true* height regardless of clipping. Used to decide top-alignment vs bottom-alignment below. - const historyContentRef = useRef(null); // Static history items can't be retroactively expanded once printed (Ink's is // append-only), so Ctrl+O doesn't edit the collapsed compact notice in place โ€” it prints a new // item with the full text on demand. This just remembers the most recent one to print. @@ -281,65 +185,6 @@ export function App({ // fixed width on the right โ€” ChatInput can't derive this from its own measured width (see the // comment on its availableColumns prop), so it's computed once here and threaded down. const chatColumns = terminalColumns - (filePanelVisible ? FILE_PANEL_WIDTH : 0); - // The split between the history viewport and the bottom section is resolved by Yoga in a single - // layout pass (history grows/shrinks, bottom is fixed) rather than computed by hand from a - // measured bottom height โ€” that measurement always lagged one frame behind the bottom section's - // actual height (useBoxMetrics updates in an effect *after* render), so on any frame it grew - // (a permission/export modal mounting, or the @-mention suggestion box opening in ChatInput) - // history was sized too tall and the bottom section overpainted it, producing the overlap. - // We still read the history viewport's own measured height, but only for the top-vs-bottom - // alignment decision โ€” a one-frame lag there only affects alignment, never the split, so it's - // harmless (unlike the split, which is what caused the overlap). - const historyViewportRef = useRef(null); - const { height: measuredHistoryHeight } = useBoxMetrics(historyViewportRef); - const { height: historyContentHeight } = useBoxMetrics(historyContentRef); - const viewportHeight = Math.max(1, measuredHistoryHeight); - // How far (in rows) the content has scrolled past the viewport โ€” the max meaningful scrollTop. - // Short conversations (content fits entirely) have maxScroll 0, which also naturally keeps them - // top-aligned instead of glued to the bottom with a gap above. - const maxScroll = Math.max(0, historyContentHeight - viewportHeight); - // Whether the view should keep tracking the latest content as it arrives (the normal chat - // behavior) or hold still at a manually scrolled position. PageUp breaks the pin; PageDown - // re-establishes it once scrolled back down to the bottom; sending a new message always re-pins. - const [pinnedToBottom, setPinnedToBottom] = useState(true); - // Rows scrolled down from the content's top edge โ€” only meaningful while not pinned; while - // pinned, the effective scrollTop is just maxScroll (always show the latest content). - const [scrollTop, setScrollTop] = useState(0); - const effectiveScrollTop = pinnedToBottom ? maxScroll : Math.min(scrollTop, maxScroll); - // Mouse selection coordinate helper: converts SGR-1006 (col, row) terminal coordinates - // to content-row coordinates relative to the history viewport's content. SGR-1006 - // reports absolute terminal positions, but content rows start at 1 within the viewport. - // The history viewport's Y position in the terminal layout offsets the mouse row. - const mouseToContentRow = useCallback((terminalRow: number): number => { - const viewportY = historyViewportRef.current - ? getAbsolutePosition(historyViewportRef.current).y - : 0; - // Content row 1 = the first row visible inside the viewport when scrollTop=0. - // When scrolled, content row = (terminal row - viewport top) + effectiveScrollTop + 1. - return (terminalRow - Math.round(viewportY)) + effectiveScrollTopRef.current + 1; - }, []); - - // Refs mirroring the scroll-relevant values so the (once-registered) mouse-wheel listener can - // read the latest without re-binding on every state change (see mouseMode effect below). - const maxScrollRef = useRef(maxScroll); - maxScrollRef.current = maxScroll; - const pinnedToBottomRef = useRef(pinnedToBottom); - pinnedToBottomRef.current = pinnedToBottom; - const scrollTopRef = useRef(scrollTop); - scrollTopRef.current = scrollTop; - const effectiveScrollTopRef = useRef(effectiveScrollTop); - effectiveScrollTopRef.current = effectiveScrollTop; - - // Scroll the history viewport by `delta` rows (negative = up, positive = down), using the same - // pin/unpin rules as the PageUp/PageDown handler above. Shared by keyboard paging and the - // mouse-wheel listener so the two paths can't drift. - const scrollBy = useCallback((delta: number) => { - const ms = maxScrollRef.current; - const current = pinnedToBottomRef.current ? ms : Math.min(scrollTopRef.current, ms); - const next = Math.max(0, Math.min(ms, current + delta)); - setScrollTop(next); - setPinnedToBottom(next >= ms); - }, []); const flushStreamingText = useCallback(() => { const accumulated = streamingAccumulatorRef.current; @@ -365,26 +210,6 @@ export function App({ getGitInfo(cwd).then(setGitInfo).catch(() => setGitInfo(null)); }, [cwd]); - // Fetch once up front; re-fetched after each turn (see submitTurn) since a tool call (git_commit, - // bash) can switch branches or change the dirty state mid-session. - // Enable xterm mouse tracking (button-event mode 1002 + SGR-1006 format) so click, drag, and - // wheel events arrive on stdin as escape sequences. ink's input parser passes each mouse sequence through to useInput - // as a single event (with the leading ESC stripped from `input`), where we detect it below. - // Only enabled during the chat phase and only when raw mode is supported; toggling it off on exit - // (and on phase change) restores the terminal so the shell's own mouse mode isn't disturbed. - // Also suspended while the export filename prompt is open: that prompt's text field is - // ink-text-input (third-party) with no guard against raw mouse sequences, so scrolling while it's - // open would otherwise type them straight into the filename โ€” simplest fix is to stop the - // terminal from sending them at all rather than filtering inside a dependency we don't control. - useEffect(() => { - if (!isRawModeSupported || phase !== "input" || exportPrompt || !mouseMode) return; - stdout.write("[?1002h[?1006h"); - setRawMode(true); - return () => { - stdout.write("[?1006l[?1002l"); - }; - }, [phase, isRawModeSupported, stdout, setRawMode, exportPrompt, mouseMode]); - // A backgrounded bash job (see Ctrl+B below) can finish long after the turn that started it has // ended โ€” this is how its completion still reaches the transcript. useEffect(() => { @@ -418,60 +243,6 @@ export function App({ // Only react to global shortcuts during the actual chat phase; ignore them while a modal // (permission/export) or a non-input phase (model/session select, connecting) is open. if (phaseRef.current !== "input" || permission || exportPrompt) return; - // Mouse events (xterm SGR-1006 format). We handle: - // - Wheel up/down: scroll the transcript - // - Shift+click/drag: not captured โ€” falls through so the terminal handles native - // text selection, which is the primary way to copy text in-app. - // All other mouse events (clicks, drags, releases) are swallowed so they don't fall through - // to text input. In the future, in-app text selection can be built on top of these events. - const mouseEvent = matchMouseSequence(input); - if (mouseEvent) { - // Shift+click/drag: don't capture โ€” let the terminal handle native text selection. - if (mouseEvent.shift) return; - const btn = logicalButton(mouseEvent); - if (mouseEvent.button === 64) { scrollBy(-3); } // wheel up - else if (mouseEvent.button === 65) { scrollBy(3); } // wheel down - else if (btn === "left" && mouseEvent.pressed) { - // Left button press: start in-app text selection - const row = mouseToContentRow(mouseEvent.row); - const start = { row, col: mouseEvent.col }; - selectionStartRef.current = start; - selectionEndRef.current = start; - setSelectionStart(start); - setSelectionEnd(start); - push({ kind: "notice", text: "Selection started โ€” drag to select text" }); - } else if (btn === "drag" && selectionStartRef.current) { - // SGR-1006 drag motion (bit 32 set): extend selection - const row = mouseToContentRow(mouseEvent.row); - const end = { row, col: mouseEvent.col }; - selectionEndRef.current = end; - setSelectionEnd(end); - } else if (btn === "release" && selectionStartRef.current) { - // Release: copy selection to clipboard, then clear it - const row = mouseToContentRow(mouseEvent.row); - const end = { row, col: mouseEvent.col }; - const start = selectionStartRef.current; - const from = start.row < end.row || (start.row === end.row && start.col <= end.col) ? start : end; - const to = start.row < end.row || (start.row === end.row && start.col <= end.col) ? end : start; - const text = extractSelectionText(staticItems, streamingText, from, to); - if (text) { - copyToClipboard(text, stdout); - push({ kind: "notice", text: `Copied ${text.split("\n").length} line(s) to clipboard` }); - } else { - push({ kind: "notice", text: "No text in selection range" }); - } - selectionStartRef.current = null; - selectionEndRef.current = null; - setSelectionStart(null); - setSelectionEnd(null); - } - return; - } - if (key.pageUp || key.pageDown) { - const pageStep = Math.max(1, viewportHeight - 1); - scrollBy(key.pageUp ? -pageStep : pageStep); - return; - } if (key.ctrl && input === "f") { // Three-state cycle: hidden -> open+focused -> open+unfocused (via Escape, not here) -> hidden. // Pressing Ctrl+F while open-but-unfocused (the Escape state) re-focuses it instead of hiding @@ -923,7 +694,6 @@ export function App({ setInputValue(""); const trimmed = raw.trim(); if (!trimmed) return; - setPinnedToBottom(true); const session = sessionRef.current; if (!session) return; @@ -1141,22 +911,6 @@ export function App({ } return; } - if (trimmed.startsWith("/mouse")) { - const arg = trimmed.slice("/mouse".length).trim().toLowerCase(); - if (!arg) { - push({ kind: "notice", text: `Mouse: ${mouseMode ? "on" : "off"}. Use /mouse on|off. When on, wheel scrolls and click+drag selects text (copied to clipboard on release). Shift+click/drag for native terminal selection. When off, native selection works but wheel does nothing in-app.` }); - } else if (arg === "on") { - setMouseMode(true); - push({ kind: "notice", text: "Mouse on โ€” wheel scrolls, click+drag selects text (auto-copied to clipboard on release). Shift+click/drag for native terminal selection." }); - } else if (arg === "off") { - setMouseMode(false); - push({ kind: "notice", text: "Mouse off โ€” native terminal selection/copy works. Use PageUp/PageDown to scroll, or /mouse on to re-enable wheel scroll." }); - } else { - push({ kind: "notice", text: `Unknown option "${arg}". Use /mouse on or /mouse off.`, isError: true }); - } - return; - } - // UserPromptSubmit hooks see the raw text before plugin-command expansion or @mention // resolution โ€” a hook can block the message outright, or inject extra context (appended // below wherever the message actually ends up going). @@ -1270,38 +1024,20 @@ export function App({ } return ( - - - {/* Scrollable, bottom-pinned viewport: overflowY="hidden" clips the content box, which is - * shifted up by a negative marginTop equal to effectiveScrollTop rows โ€” at maxScroll (the - * pinned-to-bottom default) that puts the *latest* content flush against the bottom edge, - * auto-scrolling to it without any input; PageUp/PageDown (see the useInput handler above) - * unpin and walk scrollTop up/down a page at a time. This replaced (permanent - * one-shot scrollback printing) because Static's already-flushed rows never participate in - * Yoga layout again, which is fundamentally incompatible with letting old items visually - * scroll within a *bounded* viewport. Trade-off: every item re-renders on every frame now - * (Static rendered each item exactly once, ever) โ€” fine at the sizes a single session - * reaches before auto-compaction, but worth knowing if a session gets huge. */} - - {/* flexShrink={0} is load-bearing: Yoga's default flexShrink is nonzero, so without this the - * content box (and every item inside it) gets squeezed down to the viewport's height instead - * of clipped at it โ€” Yoga distributes the deficit proportionally across every child, which - * rounds most rows down to zero height and leaves only a handful of survivors, rendering as - * scrambled/decimated lines instead of a clean top slice or bottom slice of real content. */} - - {staticItems.map((item) => ( - - ))} - {streamingText !== null && ( - - )} - {isThinking && streamingText === null && !permission && !exportPrompt && ( - - )} - - - - + <> + {/* Finished history prints once, directly to the terminal's real scrollback โ€” never + * re-rendered, never height-clipped โ€” which is what makes the terminal's own native mouse + * wheel scroll and click-drag text selection/copy work with zero app-side mouse tracking or + * virtual-scroll code (see index.tsx for why locode also stays off the alternate screen). */} + {(item) => } + + + {streamingText !== null && ( + + )} + {isThinking && streamingText === null && !permission && !exportPrompt && ( + + )} {permission ? ( ) : ( <> - {!pinnedToBottom && sessionRef.current && phaseRef.current === "input" && ( - โ”€โ”€ scrolled up ยท PageDown to jump to latest โ”€โ”€ - )} {sessionRef.current && phaseRef.current === "input" && ( )} @@ -1357,16 +1091,17 @@ export function App({ )} + setFilePanelFocused(false)} + /> - setFilePanelFocused(false)} - /> - + ); } diff --git a/src/ui/ink/ChatInput.tsx b/src/ui/ink/ChatInput.tsx index 3034187..aae70bd 100644 --- a/src/ui/ink/ChatInput.tsx +++ b/src/ui/ink/ChatInput.tsx @@ -5,7 +5,6 @@ import stringWidth from "string-width"; import { getAbsolutePosition } from "./absolutePosition.js"; import { ACCENT_HEX } from "../theme.js"; import { getActiveMention } from "../../utils/mentions.js"; -import { matchMouseSequence } from "./mouseInput.js"; // Bracketed paste markers emitted by terminals when the user pastes text. const BP_START = "\x1b[200~"; @@ -248,12 +247,6 @@ export function ChatInput({ value, onChange, onSubmit, cwd, history = [], availa }, [value, cursorOffset, replaceValue]); useInput((input, key) => { - // App.tsx's own useInput (mounted for the whole app) already handles xterm SGR mouse sequences - // (wheel scroll, clicks) for the history viewport โ€” but ink broadcasts every raw stdin event to - // every active useInput hook, so this component sees the same sequence too. Without this guard - // it falls through to the catch-all `if (input)` below and types the raw escape text into the - // chat box on every scroll notch. See mouseInput.ts. - if (matchMouseSequence(input)) return; // Shift+Tab (cycle permission mode) is handled globally in App.tsx now, not here โ€” see its // useInput handler for why. if (key.shift && key.tab) return; diff --git a/src/ui/ink/FilePanel.tsx b/src/ui/ink/FilePanel.tsx index b1e2c73..f9321ab 100644 --- a/src/ui/ink/FilePanel.tsx +++ b/src/ui/ink/FilePanel.tsx @@ -21,6 +21,11 @@ interface Props { cwd: string; touchedFiles: TouchedFile[]; width: number; + /** Explicit height (in rows) for the panel โ€” it's no longer nested inside a fixed-height + * ancestor (App.tsx's history now flows into the terminal's own scrollback via + * rather than a bounded viewport), so this panel sizes itself instead of inheriting a height + * to flexGrow against. */ + height: number; /** Whether the panel currently owns keyboard input (App.tsx disables ChatInput's own useInput * while this is true, so the same arrow/Enter/Escape keystroke doesn't do both at once). */ focused: boolean; @@ -93,7 +98,7 @@ const IGNORE = ["node_modules/**", ".git/**", "dist/**"]; // tree that's too big to show at once โ€” this cap is just a hard ceiling under that. const MAX_FILES = 2000; -export function FilePanel({ visible, activeTab, cwd, touchedFiles, width, focused, onExitFocus }: Props) { +export function FilePanel({ visible, activeTab, cwd, touchedFiles, width, height, focused, onExitFocus }: Props) { const [allFiles, setAllFiles] = useState(null); const [collapsedPaths, setCollapsedPaths] = useState>(new Set()); const [selectedIndex, setSelectedIndex] = useState(0); @@ -193,6 +198,7 @@ export function FilePanel({ visible, activeTab, cwd, touchedFiles, width, focuse flexDirection="column" flexShrink={0} width={width} + height={height} borderStyle="single" borderColor={focused ? ACCENT_HEX : "gray"} paddingX={1} diff --git a/src/ui/ink/HistoryItemView.tsx b/src/ui/ink/HistoryItemView.tsx index 4a55b06..5095619 100644 --- a/src/ui/ink/HistoryItemView.tsx +++ b/src/ui/ink/HistoryItemView.tsx @@ -35,8 +35,8 @@ const HELP_LINES = [ " Ctrl+F open/focus the file panel; press again to close it (Esc to unfocus without closing)", " Ctrl+G switch the file panel's tab (Files / Activity)", " โ†‘โ†“ โ†ต โ† โ†’ (while the file panel is focused) navigate / expand / collapse folders", - " PageUp/PageDown scroll the conversation", - " Mouse wheel scroll (click+drag to select/copy text in-app)", + " Mouse wheel / click+drag your terminal's own native scroll and text selection/copy โ€” locode", + " doesn't intercept the mouse, so this always works like any other CLI", ]; function formatDuration(ms: number): string { diff --git a/src/ui/ink/StatusBar.tsx b/src/ui/ink/StatusBar.tsx index baeebbc..a00705a 100644 --- a/src/ui/ink/StatusBar.tsx +++ b/src/ui/ink/StatusBar.tsx @@ -32,6 +32,7 @@ interface Props { createdAt: string; inputTokens: number; outputTokens: number; + modelTimeMs: number; gitInfo: GitInfo | null; } @@ -75,6 +76,7 @@ export function StatusBar({ createdAt, inputTokens, outputTokens, + modelTimeMs, gitInfo, }: Props) { // Ticks every 30s purely to keep "elapsed"/burn-rate live while otherwise idle โ€” no other state @@ -91,11 +93,11 @@ export function StatusBar({ const elapsedMs = Math.max(0, now - new Date(createdAt).getTime()); const elapsedMinutes = elapsedMs / 60_000; - // Output-only: inputTokens sums the *full resent conversation context* on every API call (no - // prompt caching for local backends), so a chatty turn with many tool-call round trips inflates - // it by call count, not by anything the user would recognize as "burn rate". outputTokens/min - // reflects actual generation throughput instead. - const burnRate = elapsedMinutes >= 0.1 ? outputTokens / elapsedMinutes : null; + // Use model time (wall-clock time spent waiting on backend responses) rather than session + // elapsed time. A session open for 30 minutes with only 2 minutes of actual model interaction + // should show the real throughput, not a diluted rate that makes the model look slow. + const modelMinutes = modelTimeMs / 60_000; + const burnRate = modelMinutes >= 0.1 ? outputTokens / modelMinutes : null; return ( diff --git a/src/ui/ink/index.tsx b/src/ui/ink/index.tsx index eec8c22..a0d7de6 100644 --- a/src/ui/ink/index.tsx +++ b/src/ui/ink/index.tsx @@ -52,25 +52,12 @@ export async function runInkApp(opts: RunInkAppOptions): Promise { // Ink render tree so cleanup() can still read it after instance.unmount() has torn everything // in App.tsx down. let currentSessionId: string | null = null; - // Whether we've switched to the terminal's alternate screen buffer โ€” tracked so cleanup() only - // switches back if we actually switched away, and so a second session starting mid-process - // (e.g. picking a different saved session via /resume) doesn't re-enter it redundantly. - let enteredAltScreen = false; - // Entering the alternate screen alone does not guarantee the cursor starts at the top-left โ€” - // some terminals carry the cursor row over from the main screen, leaving blank rows above - // Ink's first output until enough content has been printed to push past that row. Clear the - // new (blank) buffer and explicitly home the cursor so content always starts flush at (0, 0). - const ALT_SCREEN_ENTER = "[?1049h" + "" + ""; - const ALT_SCREEN_EXIT = "[?1049l"; - - // Start the UI immediately โ€” no blocking network calls before rendering. - // Model listing happens inside the App component so the user sees the UI right away. - // - // The model-select/connecting phases before a session exists stay on the main screen (so any - // startup errors remain in normal scrollback); once a session actually starts, we switch to the - // alternate screen buffer for a clean full-screen chat view. This trades away the terminal's own - // scrollback (no mouse-wheel/Shift+PgUp once in the alt screen) for that full-screen feel โ€” App.tsx - // provides its own in-app scrollback instead (PageUp/PageDown over the history viewport). + // Start the UI immediately โ€” no blocking network calls before rendering. Model listing happens + // inside the App component so the user sees the UI right away. Deliberately stays on the + // terminal's normal screen buffer (never the alternate screen) the whole time โ€” finished + // history prints as permanent scrollback (see App.tsx's use of Ink's ), so the + // terminal's own native mouse-wheel scroll and click-drag text selection/copy just work, with + // no app-side mouse tracking or virtual-scroll machinery needed. const instance = render( { extraToolsPromise={extraToolsPromise} onSessionIdChange={(id) => { currentSessionId = id; - if (!enteredAltScreen) { - enteredAltScreen = true; - process.stdout.write(ALT_SCREEN_ENTER); - } }} />, ); @@ -99,12 +82,6 @@ export async function runInkApp(opts: RunInkAppOptions): Promise { const cleanup = async () => { if (cleanedUp) return; cleanedUp = true; - // Leave the alternate screen first (if we ever entered it) so everything printed below โ€” - // hook errors, the resume hint โ€” lands on the user's normal scrollback, not a screen that's - // about to disappear. - if (enteredAltScreen) { - process.stdout.write(ALT_SCREEN_EXIT); - } // Flush in-flight autosaves first so a fire-and-forget persist right before exit isn't lost. await flushPendingSaves(); // Best-effort: don't leave backgrounded shells (dev servers, watch builds) running as orphans. diff --git a/src/ui/ink/mouseInput.ts b/src/ui/ink/mouseInput.ts deleted file mode 100644 index ea6f5d3..0000000 --- a/src/ui/ink/mouseInput.ts +++ /dev/null @@ -1,105 +0,0 @@ -// xterm SGR-1006 mouse sequences (enabled in App.tsx via `\x1b[?1002h\x1b[?1006h`) -// arrive on stdin as a single ink `useInput` event with the leading ESC stripped, -// e.g. "[<0;12;5M" (left button press at col 12, row 5) or "[<64;12;5M" (wheel up). -// -// Mode 1002 (button-event tracking) reports: button press, button release, and -// drag (motion while a button is held). Every active `useInput` hook in the tree -// receives the same raw event โ€” ink has no concept of one handler "consuming" it -// before others see it โ€” so any text-input component that doesn't recognize the -// sequence falls through to its regular-character handling and types the raw -// escape text into the field. Shared so every `useInput` consumer can ignore it -// the same way App.tsx's own handler already does. -// -// SGR-1006 format: [\x07 (target 'c' = system clipboard) - const base64 = Buffer.from(text, "utf-8").toString("base64"); - // Write in chunks to avoid overflowing terminal input buffers (some terminals - // have limits around 4096 bytes for a single escape sequence). - const CHUNK = 300; - for (let i = 0; i < base64.length; i += CHUNK) { - const chunk = base64.slice(i, i + CHUNK); - // For the first chunk, start the sequence; for subsequent chunks, just append. - if (i === 0) { - stdout.write(`\x1b]52;c;${chunk}`); - } else { - stdout.write(chunk); - } - } - stdout.write("\x07"); // BEL terminates the sequence -} \ No newline at end of file diff --git a/tsup.config.ts b/tsup.config.ts index 3f9891b..c2f0d80 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from "tsup"; export default defineConfig({ entry: ["src/cli.ts"], format: ["esm"], - target: "node20", + target: "node22", clean: true, banner: { js: "#!/usr/bin/env node",