feat: split system prompt by local/cloud, improve mouse tracking, increase retries & iterations
- Separate system prompt for local vs cloud models via isLocal flag (isLocalBackendURL, buildSystemPrompt(..., isLocal), propagate through createSession/createSessionFromRecord/compactSession/spawnSubAgent) - Increase MAX_EMPTY_RESPONSE_RETRIES from 1 to 3 for cloud model resilience - Upgrade mouse input: full SGR-1006 parsing with col/row/pressed/modifiers, logicalButton() helper, and copyToClipboard() via OSC 52 - Add temporary mouse debug logging in App.tsx - Increase DEFAULT_MAX_ITERATIONS from 100 to 300 - Update README mouse/scrollback docs, tweak diff-remove color
This commit is contained in:
@@ -115,7 +115,7 @@ Note: even models with genuine native tool-calling support occasionally emit a t
|
||||
/model <name> switch the model used for the current backend
|
||||
/backend <name> switch backend (ollama | lmstudio), keeps current model
|
||||
/mode <name> view or force tool-call mode (native | fallback)
|
||||
/mouse [on|off] toggle mouse-wheel scroll (off by default so terminal text selection/copy works)
|
||||
/mouse [on|off] toggle mouse tracking (on by default; hold Shift+click/drag for native text selection)
|
||||
/perm [mode] cycle or set permission mode (default | plan | auto-edit | auto-accept)
|
||||
/status show current model, backend, tool-call mode, and cwd
|
||||
/dashboard show session stats: token I/O, elapsed/model time, turns, tool calls
|
||||
@@ -161,7 +161,7 @@ locode config path
|
||||
- Requires a real interactive terminal (TTY) — you can't pipe input into it or run it from a non-interactive script.
|
||||
- Native tool-calling reliability varies by model and is non-deterministic even for capable models (see above).
|
||||
- No OS-level sandboxing (no container/VM isolation) — mutating tools operate on the real filesystem/shell with the permissions of the user running `locode`. Only approve commands you understand. Two lightweight guardrails run unconditionally regardless of permission mode (including `auto-accept`), as a safety floor rather than a full sandbox: `write_file`/`edit_file`/`bash`'s `cwd` override can't target a path outside the working directory (`../` traversal, an absolute path elsewhere, or — on Windows — a different drive all refuse), and `bash` refuses a short list of unambiguously catastrophic commands (wiping the filesystem root or home directory, a fork bomb, formatting/wiping a whole drive, writing raw data to a block device) before they'd ever run. Neither guard stops a model from doing damage confined to *within* the project directory, or running something merely inadvisable — see `src/tools/pathGuard.ts` and `src/tools/bashGuard.ts`.
|
||||
- In-app scrollback is manual: PageUp/PageDown scroll the conversation view a page at a time (the terminal's native scrollback isn't available in the alternate screen buffer). Scrolling back up unpins the view from the latest message; PageDown back to the bottom (or sending a new message) re-pins it so new messages auto-scroll into view again.
|
||||
- In-app scrollback: mouse wheel scrolls the conversation view when mouse tracking is on (the default). PageUp/PageDown also scroll a page at a time. Hold Shift+click/drag for native terminal text selection and copy (when mouse tracking is on). Scrolling back up unpins the view from the latest message; scrolling back to the bottom (or sending a new message) re-pins it so new messages auto-scroll into view. Toggle mouse tracking with `/mouse on|off`.
|
||||
- Windows shell quoting for the `bash` tool has only had light testing; behavior may differ from Unix shells for complex quoting.
|
||||
- `git_commit` covers add/commit/create_branch/checkout/push/reset/stash/merge/rebase/delete_branch. Use `bash` for anything beyond that.
|
||||
- MCP tool results support text, image, audio, and resource content blocks. Images are returned in the same shape as `read_file` so vision-capable models can see them; audio and binary resources are summarized. Remote (HTTP) MCP servers support static headers (e.g. a bearer token) but not OAuth flows.
|
||||
|
||||
@@ -511,3 +511,41 @@ handler와 preview 양쪽의 `occurrences === 0` 경로에 적용. 기존 "not f
|
||||
### 인코딩 메모
|
||||
- `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 세션에서 변경됨)
|
||||
|
||||
@@ -121,8 +121,8 @@ describe("runTurn / empty response retry", () => {
|
||||
|
||||
const session = createSession(fakeClient, "test-model", process.cwd(), async () => "once", "native", []);
|
||||
await expect(runTurn(session, "hi", () => {})).rejects.toThrow("Empty response from model.");
|
||||
// One initial empty + one retry = 2 calls total.
|
||||
expect(call).toBe(2);
|
||||
// One initial empty + three retries = 4 calls total.
|
||||
expect(call).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+5
-4
@@ -127,7 +127,7 @@ 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
|
||||
* once with a short nudge so the model continues, mirroring the malformed-tool-call path. */
|
||||
const MAX_EMPTY_RESPONSE_RETRIES = 1;
|
||||
const MAX_EMPTY_RESPONSE_RETRIES = 3;
|
||||
|
||||
// Sub-agent safety limits. The toolset already excludes `agent` for sub-agents (so a model can't
|
||||
// spawn nested sub-agents through normal tool use), but these are independent, explicit backstops
|
||||
@@ -327,7 +327,7 @@ export async function compactSession(session: Session): Promise<string> {
|
||||
|
||||
if (summary) {
|
||||
session.messages = [
|
||||
{ role: "system", content: buildSystemPrompt(session.toolset.tools, session.mode, session.projectInstructions) },
|
||||
{ role: "system", content: buildSystemPrompt(session.toolset.tools, session.mode, session.projectInstructions, session.isLocal) },
|
||||
{ role: "assistant", content: `[Earlier conversation compacted to save context]\n\n${summary}` },
|
||||
...tail,
|
||||
];
|
||||
@@ -335,7 +335,7 @@ export async function compactSession(session: Session): Promise<string> {
|
||||
// Nothing to summarize: keep the tail as-is, but still rebuild the system prompt in case
|
||||
// the tail's first message was a stale system prompt we want to replace.
|
||||
session.messages = [
|
||||
{ role: "system", content: buildSystemPrompt(session.toolset.tools, session.mode, session.projectInstructions) },
|
||||
{ role: "system", content: buildSystemPrompt(session.toolset.tools, session.mode, session.projectInstructions, session.isLocal) },
|
||||
...tail,
|
||||
];
|
||||
}
|
||||
@@ -692,7 +692,7 @@ async function runSubAgentTurn(
|
||||
const subToolset = buildToolSet(restricted);
|
||||
const systemPrompt = overrides?.systemPrompt
|
||||
? `${overrides.systemPrompt}\n\nAvailable tools:\n${subToolset.tools.map((t) => `- ${t.name}: ${t.description}`).join("\n")}`
|
||||
: `${buildSystemPrompt(subToolset.tools, parent.mode, parent.projectInstructions)}\n\nYou are a sub-agent handling one focused task delegated by another assistant. Only the final text you return will be seen — not your intermediate tool calls — so make your answer complete and self-contained.`;
|
||||
: `${buildSystemPrompt(subToolset.tools, parent.mode, parent.projectInstructions, parent.isLocal)}\n\nYou are a sub-agent handling one focused task delegated by another assistant. Only the final text you return will be seen — not your intermediate tool calls — so make your answer complete and self-contained.`;
|
||||
const subMessages: ChatCompletionMessageParam[] = [{ role: "system", content: systemPrompt }];
|
||||
const subSession: Session = {
|
||||
id: randomUUID(),
|
||||
@@ -701,6 +701,7 @@ async function runSubAgentTurn(
|
||||
model: parent.model,
|
||||
cwd: parent.cwd,
|
||||
mode: parent.mode,
|
||||
isLocal: parent.isLocal,
|
||||
messages: subMessages,
|
||||
maxIterations: parent.maxIterations,
|
||||
permissions: parent.permissions,
|
||||
|
||||
+12
-3
@@ -100,6 +100,11 @@ export interface Session {
|
||||
* so a top-level turn and its sub-agents all funnel through the same queue. Initialized as a
|
||||
* resolved promise so the first caller doesn't wait on anything. */
|
||||
mutationGate: Promise<void>;
|
||||
/** True when the backend is a local server (Ollama / LM Studio on localhost). Used to
|
||||
* tailor the system prompt — local models need extra guidance about their limitations;
|
||||
* cloud models get a leaner prompt without self-fulfilling "you may produce empty responses"
|
||||
* framing. Derived from the baseURL by isLocalBackendURL(). */
|
||||
isLocal: boolean;
|
||||
}
|
||||
|
||||
export function createSession(
|
||||
@@ -114,10 +119,11 @@ export function createSession(
|
||||
maxIterations: number = DEFAULT_MAX_ITERATIONS,
|
||||
autoCompactThreshold: number = DEFAULT_AUTO_COMPACT_THRESHOLD,
|
||||
projectInstructions: string | null = null,
|
||||
isLocal: boolean = true,
|
||||
): Session {
|
||||
const toolset = buildToolSet(tools);
|
||||
const messages: ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: buildSystemPrompt(toolset.tools, mode, projectInstructions) },
|
||||
{ role: "system", content: buildSystemPrompt(toolset.tools, mode, projectInstructions, isLocal) },
|
||||
];
|
||||
return {
|
||||
id: randomUUID(),
|
||||
@@ -126,6 +132,7 @@ export function createSession(
|
||||
model,
|
||||
cwd,
|
||||
mode,
|
||||
isLocal,
|
||||
messages,
|
||||
maxIterations,
|
||||
permissions: new PermissionManager(),
|
||||
@@ -160,10 +167,11 @@ export function createSessionFromRecord(
|
||||
maxIterations: number = DEFAULT_MAX_ITERATIONS,
|
||||
autoCompactThreshold: number = DEFAULT_AUTO_COMPACT_THRESHOLD,
|
||||
projectInstructions: string | null = null,
|
||||
isLocal: boolean = true,
|
||||
): Session {
|
||||
const toolset = buildToolSet(tools);
|
||||
const messages: ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: buildSystemPrompt(toolset.tools, record.mode, projectInstructions) },
|
||||
{ role: "system", content: buildSystemPrompt(toolset.tools, record.mode, projectInstructions, isLocal) },
|
||||
...record.messages,
|
||||
];
|
||||
const session: Session = {
|
||||
@@ -173,6 +181,7 @@ export function createSessionFromRecord(
|
||||
model: record.model,
|
||||
cwd,
|
||||
mode: record.mode,
|
||||
isLocal,
|
||||
messages,
|
||||
maxIterations,
|
||||
permissions: new PermissionManager(),
|
||||
@@ -248,5 +257,5 @@ export function undoLastTurn(session: Session): number {
|
||||
|
||||
export function setMode(session: Session, mode: ToolCallMode): void {
|
||||
session.mode = mode;
|
||||
session.messages[0] = { role: "system", content: buildSystemPrompt(session.toolset.tools, mode, session.projectInstructions) };
|
||||
session.messages[0] = { role: "system", content: buildSystemPrompt(session.toolset.tools, mode, session.projectInstructions, session.isLocal) };
|
||||
}
|
||||
|
||||
@@ -23,8 +23,16 @@ describe("buildSystemPrompt", () => {
|
||||
expect(prompt).toContain("Mutating");
|
||||
});
|
||||
|
||||
it("includes core principles", () => {
|
||||
const prompt = buildSystemPrompt([readTool], "native");
|
||||
it("includes core principles for local models", () => {
|
||||
const prompt = buildSystemPrompt([readTool], "native", null, true);
|
||||
expect(prompt).toContain("Inspect before answering");
|
||||
expect(prompt).toContain("Prefer small, targeted edits");
|
||||
expect(prompt).toContain("Recovery over retry");
|
||||
expect(prompt).toContain("Respect confirmation");
|
||||
});
|
||||
|
||||
it("includes core principles for cloud models", () => {
|
||||
const prompt = buildSystemPrompt([readTool], "native", null, false);
|
||||
expect(prompt).toContain("Inspect before answering");
|
||||
expect(prompt).toContain("Prefer small, targeted edits");
|
||||
expect(prompt).toContain("Recovery over retry");
|
||||
@@ -39,26 +47,48 @@ describe("buildSystemPrompt", () => {
|
||||
expect(prompt).toContain("agent");
|
||||
});
|
||||
|
||||
it("includes local model guidance", () => {
|
||||
const prompt = buildSystemPrompt([readTool], "native");
|
||||
it("includes local model guidance when isLocal is true", () => {
|
||||
const prompt = buildSystemPrompt([readTool], "native", null, true);
|
||||
expect(prompt).toContain("local model");
|
||||
expect(prompt).toContain("Context windows are smaller");
|
||||
expect(prompt).toContain("Working with local models");
|
||||
});
|
||||
|
||||
it("includes safety guidelines", () => {
|
||||
it("excludes local model guidance when isLocal is false (cloud)", () => {
|
||||
const prompt = buildSystemPrompt([readTool], "native", null, false);
|
||||
expect(prompt).not.toContain("local model");
|
||||
expect(prompt).not.toContain("Context windows are smaller");
|
||||
expect(prompt).not.toContain("Working with local models");
|
||||
expect(prompt).not.toContain("Empty or malformed responses");
|
||||
});
|
||||
|
||||
it("defaults to local prompt when isLocal is not specified", () => {
|
||||
const prompt = buildSystemPrompt([readTool], "native");
|
||||
expect(prompt).toContain(".git");
|
||||
expect(prompt).toContain("destructive");
|
||||
expect(prompt).toContain("local model");
|
||||
});
|
||||
|
||||
it("includes safety guidelines for both local and cloud", () => {
|
||||
const localPrompt = buildSystemPrompt([readTool], "native", null, true);
|
||||
const cloudPrompt = buildSystemPrompt([readTool], "native", null, false);
|
||||
expect(localPrompt).toContain(".git");
|
||||
expect(localPrompt).toContain("destructive");
|
||||
expect(cloudPrompt).toContain(".git");
|
||||
expect(cloudPrompt).toContain("destructive");
|
||||
});
|
||||
|
||||
it("includes fallback instructions when mode is fallback", () => {
|
||||
const prompt = buildSystemPrompt([readTool], "fallback");
|
||||
expect(prompt).toContain("tool_call");
|
||||
expect(prompt).toContain("fallback");
|
||||
expect(prompt.toLowerCase()).toContain("fallback");
|
||||
});
|
||||
|
||||
it("includes native mode instructions when mode is native", () => {
|
||||
const prompt = buildSystemPrompt([readTool], "native");
|
||||
it("includes native mode instructions when mode is native (local)", () => {
|
||||
const prompt = buildSystemPrompt([readTool], "native", null, true);
|
||||
expect(prompt).toContain("native tool-call mode");
|
||||
});
|
||||
|
||||
it("includes native mode instructions when mode is native (cloud)", () => {
|
||||
const prompt = buildSystemPrompt([readTool], "native", null, false);
|
||||
expect(prompt).toContain("native tool-call mode");
|
||||
});
|
||||
|
||||
|
||||
@@ -17,9 +17,21 @@ function formatToolList(tools: ToolDef[]): string {
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
export function buildSystemPrompt(tools: ToolDef[], mode: ToolCallMode, projectInstructions?: string | null): string {
|
||||
export function buildSystemPrompt(tools: ToolDef[], mode: ToolCallMode, projectInstructions?: string | null, isLocal: boolean = true): string {
|
||||
const toolList = formatToolList(tools);
|
||||
const base = `You are a helpful local coding assistant with access to tools for exploring and editing a codebase on the user's machine. You run against a local model served via Ollama or LM Studio, which means you may have a smaller context window and less reliable tool-call formatting than large cloud models — adapt your behavior accordingly.
|
||||
|
||||
const base = isLocal
|
||||
? buildLocalPrompt(toolList, mode)
|
||||
: buildCloudPrompt(toolList, mode);
|
||||
|
||||
return projectInstructions ? `${base}\n\n${projectInstructions}` : base;
|
||||
}
|
||||
|
||||
/** System prompt for local models (Ollama / LM Studio) — includes extra guidance about their
|
||||
* limitations (smaller context windows, unreliable tool-call formatting, empty/malformed
|
||||
* responses). These models need the hints; cloud models do not, and may be confused by them. */
|
||||
function buildLocalPrompt(toolList: string, mode: ToolCallMode): string {
|
||||
return `You are a helpful local coding assistant with access to tools for exploring and editing a codebase on the user's machine. You run against a local model served via Ollama or LM Studio, which means you may have a smaller context window and less reliable tool-call formatting than large cloud models — adapt your behavior accordingly.
|
||||
|
||||
## Available tools
|
||||
|
||||
@@ -74,7 +86,56 @@ ${mode === "fallback" ? FALLBACK_TOOL_INSTRUCTIONS : "You are in native tool-cal
|
||||
- Do not delete large sections of code without clear justification and user confirmation.
|
||||
- When running bash commands, prefer read-only inspections (ls, cat, git status) over destructive operations (rm, git reset --hard).
|
||||
- If unsure about a destructive action, ask the user first rather than proceeding.`;
|
||||
}
|
||||
|
||||
const withMode = mode === "fallback" ? base : base;
|
||||
return projectInstructions ? `${withMode}\n\n${projectInstructions}` : withMode;
|
||||
/** System prompt for cloud models (large context window, reliable tool calls, no local-model quirks).
|
||||
* Leaner than the local prompt — skips the "Working with local models" section entirely and uses
|
||||
* a more direct tone, since cloud models don't need hand-holding about their own limitations. */
|
||||
function buildCloudPrompt(toolList: string, mode: ToolCallMode): string {
|
||||
return `You are a coding assistant with access to tools for exploring and editing a codebase on the user's machine.
|
||||
|
||||
## Available tools
|
||||
|
||||
${toolList}
|
||||
|
||||
## Core principles
|
||||
|
||||
1. **Inspect before answering.** Never guess file contents, function signatures, or directory structures — use read_file, list_files, grep, or definition to verify. Stale assumptions are worse than an extra tool call.
|
||||
|
||||
2. **Prefer small, targeted edits.** Use edit_file (or multi_edit for several changes in one file) for surgical changes. Use write_file only for new files or full rewrites. edit_file requires old_string to match exactly — copy the exact text from the file (read it first), including indentation and blank lines.
|
||||
|
||||
3. **Preserve existing style.** Match the surrounding code's indentation, naming conventions, quotes, and formatting. Don't reformat code outside the change scope.
|
||||
|
||||
4. **Keep answers concise.** When you have enough information, respond in plain text — don't pad with pleasantries or restated context. Code explanations should be brief and focused on the "why", not the "what" (the code already says what).
|
||||
|
||||
5. **Recovery over retry.** If a tool call fails (edit_file "not found", bash non-zero exit, etc.), read the file or check the error output before retrying — don't repeat the same call. If edit_file suggests a closest match, use that text exactly.
|
||||
|
||||
6. **Respect confirmation.** Mutating tools (write_file, edit_file, multi_edit, notebook_edit, bash, git_commit) require user confirmation — you will see a permission prompt. Plan your edits so the user sees a clear, concise preview.
|
||||
|
||||
## Tool usage guide
|
||||
|
||||
- **read_file**: Start here. Use offset/limit for large files. Always read before editing.
|
||||
- **list_files**: Explore directory structure. Supports glob patterns like "src/**/*.ts".
|
||||
- **grep**: Search file contents. Prefer over read_file when you know what you're looking for.
|
||||
- **definition / references / diagnostics**: LSP-powered code intelligence. Use definition to find where a symbol is declared, references for all usages, diagnostics for type errors.
|
||||
- **edit_file**: For small changes to existing files. old_string must match exactly — include enough surrounding context to be unique. On mismatch, the tool suggests the closest similar text.
|
||||
- **multi_edit**: Apply several edits to the same file in one call. Each edit sees the result of previous edits, so adjust old_string for context shifts.
|
||||
- **write_file**: For new files or complete rewrites. Overwrites the entire file — use with care.
|
||||
- **bash**: Run shell commands. Prefer targeted tools (grep, definition) over broad shell commands when possible.
|
||||
- **git_status / git_commit**: Inspect repo state and commit changes. Always check status before committing.
|
||||
- **web_search / web_fetch**: Look up information not in the local codebase. For API docs, error messages, or unfamiliar libraries.
|
||||
- **agent**: Delegate a sub-task to a focused sub-agent. Good for researching many files in parallel. Sub-agents cannot spawn further sub-agents.
|
||||
- **task_create / task_list / task_get / task_update**: Track structured work items with dependencies. Use for multi-step tasks (3+ steps) so progress is visible.
|
||||
- **todo_write**: Simple checklist for progress tracking. Good for linear step-by-step work.
|
||||
|
||||
## ${mode === "fallback" ? "Fallback mode" : "Tool calling"}
|
||||
|
||||
${mode === "fallback" ? FALLBACK_TOOL_INSTRUCTIONS : "You are in native tool-call mode. Call tools using the standard function-calling format. You may call multiple read-only tools in parallel, but mutating tools are always run sequentially."}
|
||||
|
||||
## Safety
|
||||
|
||||
- Do not modify .git directories or other version-control internals.
|
||||
- Do not delete large sections of code without clear justification and user confirmation.
|
||||
- When running bash commands, prefer read-only inspections (ls, cat, git status) over destructive operations (rm, git reset --hard).
|
||||
- If unsure about a destructive action, ask the user first rather than proceeding.`;
|
||||
}
|
||||
+18
-5
@@ -8,6 +8,18 @@ export const KNOWN_BACKENDS = {
|
||||
|
||||
export type BackendName = keyof typeof KNOWN_BACKENDS;
|
||||
|
||||
/** Returns true if the given base URL looks like a local backend (Ollama or LM Studio on localhost).
|
||||
* Used to tailor the system prompt — local models get extra guidance about their limitations,
|
||||
* while cloud models (which are more capable and reliable) get a leaner prompt without that framing. */
|
||||
export function isLocalBackendURL(baseURL: string): boolean {
|
||||
try {
|
||||
const url = new URL(baseURL);
|
||||
return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Used when the context window can't be auto-detected from the backend (see backend/contextWindow.ts)
|
||||
* and the user hasn't configured one — a conservative size common among smaller local models. */
|
||||
export const DEFAULT_CONTEXT_WINDOW = 8192;
|
||||
@@ -25,11 +37,12 @@ export const DEFAULT_MAX_OUTPUT_TOKENS = 8192;
|
||||
* is one model generation request (one tool-call round-trip), and local models commonly issue a
|
||||
* single tool call per request — so a real multi-file task (read several files, edit each, grep
|
||||
* to verify, re-read) easily needs 40–60 requests. 50 was too tight and caused frequent
|
||||
* "Paused after 50 steps" soft-stops on legitimate work; 100 gives real tasks room to finish
|
||||
* while still bounding a genuinely stuck model. Hitting the cap is a soft pause, not a failure
|
||||
* (the work so far is intact — send another message to resume). Configurable via `maxIterations`,
|
||||
* e.g. `locode config set maxIterations 200` for large batch jobs. */
|
||||
export const DEFAULT_MAX_ITERATIONS = 100;
|
||||
* "Paused after 50 steps" soft-stops on legitimate work; 100 still caused frequent pauses on
|
||||
* larger tasks. 300 gives real tasks ample room to finish while still bounding a genuinely stuck
|
||||
* model. Hitting the cap is a soft pause, not a failure (the work so far is intact — send another
|
||||
* message to resume). Configurable via `maxIterations`, e.g. `locode config set maxIterations 500`
|
||||
* for very large batch jobs. */
|
||||
export const DEFAULT_MAX_ITERATIONS = 300;
|
||||
|
||||
/** Fraction of the context window at which locode automatically summarizes the conversation.
|
||||
* User-configurable via `locode config set autoCompactThreshold`. */
|
||||
|
||||
+6
-1
@@ -27,7 +27,7 @@ import { setCachedMode } from "../../backend/capabilityCache.js";
|
||||
import { resolveContextWindow } from "../../backend/contextWindow.js";
|
||||
import { resolveToolCallMode } from "../../backend/resolveMode.js";
|
||||
import { resolveAutoCompactThreshold, resolveMaxIterations } from "../../config/config.js";
|
||||
import { KNOWN_BACKENDS, type BackendName } from "../../config/defaults.js";
|
||||
import { KNOWN_BACKENDS, type BackendName, isLocalBackendURL } from "../../config/defaults.js";
|
||||
import { getMcpStatuses, reconnectMcpServers } from "../../mcp/manager.js";
|
||||
import type { PermissionDecision, PermissionMode } from "../../permissions/types.js";
|
||||
import { defaultExportFilename, exportSession, type ExportFormat } from "../../persistence/exportSession.js";
|
||||
@@ -407,6 +407,9 @@ export function App({
|
||||
// to text input. In the future, in-app text selection can be built on top of these events.
|
||||
const mouseEvent = matchMouseSequence(input);
|
||||
if (mouseEvent) {
|
||||
// DEBUG: log all mouse events
|
||||
process.stderr.write(`[MOUSE] button=${mouseEvent.button} pressed=${mouseEvent.pressed} col=${mouseEvent.col} row=${mouseEvent.row} shift=${mouseEvent.shift} logicalBtn=${logicalButton(mouseEvent)}
|
||||
`);
|
||||
// Shift+click/drag: don't capture — let the terminal handle native text selection.
|
||||
if (mouseEvent.shift) return;
|
||||
if (mouseEvent.button === 64) scrollBy(-3); // wheel up
|
||||
@@ -562,6 +565,7 @@ export function App({
|
||||
resolveMaxIterations(),
|
||||
resolveAutoCompactThreshold(),
|
||||
projectInstructions,
|
||||
isLocalBackendURL(baseURLRef.current),
|
||||
);
|
||||
onSessionIdChange?.(sessionRef.current.id);
|
||||
push({ kind: "banner", cwd, model, backend: baseURLRef.current });
|
||||
@@ -610,6 +614,7 @@ export function App({
|
||||
resolveMaxIterations(),
|
||||
resolveAutoCompactThreshold(),
|
||||
projectInstructions,
|
||||
isLocalBackendURL(record.baseURL),
|
||||
);
|
||||
onSessionIdChange?.(sessionRef.current.id);
|
||||
push({
|
||||
|
||||
+87
-11
@@ -1,13 +1,89 @@
|
||||
// xterm SGR-1006 mouse sequences (enabled in App.tsx via `\x1b[?1000h\x1b[?1006h`) arrive on stdin
|
||||
// as a single ink `useInput` event with the leading ESC stripped, e.g. "[<64;12;5M" (wheel up) or
|
||||
// "[<0;12;5M" (left button down). 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 scroll handler already does.
|
||||
const MOUSE_SEQUENCE_RE = /^\[<(\d+);\d+;\d+[Mm]$/;
|
||||
// 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: [<button;col;rowM (press/drag) or [<button;col;rowm (release)
|
||||
// button encodes button number + modifier keys:
|
||||
// bit 0: shift
|
||||
// bit 1: meta (alt)
|
||||
// bit 2: ctrl
|
||||
// bits 4-5: 00=left, 01=middle, 10=right
|
||||
// Special: 64=wheel up, 65=wheel down
|
||||
// Drag reports button 3 (all buttons released) during motion.
|
||||
|
||||
export function matchMouseSequence(input: string): { button: number } | null {
|
||||
const match = MOUSE_SEQUENCE_RE.exec(input);
|
||||
return match ? { button: Number(match[1]) } : null;
|
||||
const MOUSE_SEQUENCE_RE = /^\[<(\d+);(\d+);(\d+)([Mm])$/;
|
||||
|
||||
export interface MouseEvent {
|
||||
/** Raw button code (includes modifier bits in bits 0-2, button in bits 4-5). */
|
||||
button: number;
|
||||
/** 1-based column. */
|
||||
col: number;
|
||||
/** 1-based row. */
|
||||
row: number;
|
||||
/** True on press/drag (trailing 'M'), false on release (trailing 'm'). */
|
||||
pressed: boolean;
|
||||
/** True if shift was held. */
|
||||
shift: boolean;
|
||||
/** True if meta (alt) was held. */
|
||||
meta: boolean;
|
||||
/** True if ctrl was held. */
|
||||
ctrl: boolean;
|
||||
}
|
||||
|
||||
/** Which logical button is pressed (ignoring modifiers). */
|
||||
export function logicalButton(e: MouseEvent): "left" | "middle" | "right" | "wheel-up" | "wheel-down" | "release" | "other" {
|
||||
const raw = e.button & ~0x07; // strip modifier bits
|
||||
if (raw === 0) return "left";
|
||||
if (raw === 1) return "middle";
|
||||
if (raw === 2) return "right";
|
||||
if (raw === 64) return "wheel-up";
|
||||
if (raw === 65) return "wheel-down";
|
||||
if (raw === 3 || (!e.pressed && raw === 0)) return "release";
|
||||
return "other";
|
||||
}
|
||||
|
||||
export function matchMouseSequence(input: string): MouseEvent | null {
|
||||
const match = MOUSE_SEQUENCE_RE.exec(input);
|
||||
if (!match) return null;
|
||||
const button = Number(match[1]);
|
||||
const col = Number(match[2]);
|
||||
const row = Number(match[3]);
|
||||
const pressed = match[4] === "M";
|
||||
return {
|
||||
button,
|
||||
col,
|
||||
row,
|
||||
pressed,
|
||||
shift: !!(button & 4),
|
||||
meta: !!(button & 8),
|
||||
ctrl: !!(button & 16),
|
||||
};
|
||||
}
|
||||
|
||||
/** Write text to the system clipboard using OSC 52 (works over SSH, tmux, etc.).
|
||||
* Falls back to platform-specific commands if OSC 52 doesn't work. */
|
||||
export function copyToClipboard(text: string, stdout: NodeJS.WriteStream): void {
|
||||
// OSC 52: \x1b]52;c;<base64>\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
|
||||
}
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
export const ACCENT_HEX = "#D97757";
|
||||
export const DIFF_ADD_HEX = "#2ea043";
|
||||
export const DIFF_REMOVE_HEX = "#f85149";
|
||||
export const DIFF_REMOVE_HEX = "#e8904e";
|
||||
|
||||
Reference in New Issue
Block a user