@kmugalkhod/lightcode 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +119 -91
  2. package/cli.js +633 -236
  3. package/package.json +1 -1
  4. package/server.js +220 -104
package/cli.js CHANGED
@@ -124,7 +124,7 @@ var init_logger = __esm(() => {
124
124
  });
125
125
 
126
126
  // packages/shared/src/version.ts
127
- var lightcodeVersion = "0.6.0";
127
+ var lightcodeVersion = "0.7.1";
128
128
 
129
129
  // packages/shared/src/index.ts
130
130
  var productName = "Lightcode";
@@ -1053,7 +1053,7 @@ var init_slash_page_menu = __esm(() => {
1053
1053
  });
1054
1054
 
1055
1055
  // packages/ai/src/constants.ts
1056
- var MAX_TOOL_TEXT_OUTPUT_CHARS = 12000, DEFAULT_TOOL_TEXT_OUTPUT_CHARS = 6000, MAX_TOOL_LIST_ENTRIES = 200, MAX_TOOL_SEARCH_RESULTS = 200, ANTHROPIC_TOOL_OPTIONAL_PARAMETER_BUDGET = 24;
1056
+ var MAX_TOOL_TEXT_OUTPUT_CHARS = 12000, DEFAULT_TOOL_TEXT_OUTPUT_CHARS = 6000, MAX_TOOL_LIST_ENTRIES = 200, MAX_TOOL_SEARCH_RESULTS = 200, ANTHROPIC_TOOL_OPTIONAL_PARAMETER_BUDGET = 24, TODO_CONTENT_MAX_CHARS = 200, TODO_ACTIVE_FORM_MAX_CHARS = 80, PROGRESS_NOTE_PRUNE_MIN_CHARS = 200;
1057
1057
 
1058
1058
  // packages/ai/src/coding-agent-modes.ts
1059
1059
  import { z } from "zod";
@@ -1970,20 +1970,22 @@ var init_schema10 = __esm(() => {
1970
1970
  todoPrioritySchema = z14.enum(["low", "medium", "high"]);
1971
1971
  todoItemSchema = z14.object({
1972
1972
  id: z14.string().min(1).max(120).optional(),
1973
- content: z14.string().min(1).max(500),
1973
+ content: z14.string().min(1).max(TODO_CONTENT_MAX_CHARS),
1974
1974
  status: todoStatusSchema,
1975
1975
  priority: todoPrioritySchema.optional().default("medium"),
1976
- notes: z14.string().max(1000).optional()
1976
+ notes: z14.string().max(1000).optional(),
1977
+ activeForm: z14.string().min(1).max(TODO_ACTIVE_FORM_MAX_CHARS).optional()
1977
1978
  });
1978
1979
  todoItemProviderSchema = z14.object({
1979
- content: z14.string().min(1).max(500),
1980
- status: todoStatusSchema
1980
+ content: z14.string().min(1).max(TODO_CONTENT_MAX_CHARS),
1981
+ status: todoStatusSchema,
1982
+ activeForm: z14.string().min(1).max(TODO_ACTIVE_FORM_MAX_CHARS).optional().describe(`Short present-tense active label for this task, e.g. "Adding tests" or "Editing schema". Set it primarily for the in_progress item so the UI can show what's actively happening; the UI falls back to content when omitted.`)
1981
1983
  });
1982
1984
  todoWriteInputSchema = z14.object({
1983
1985
  todos: z14.array(todoItemSchema).max(50)
1984
1986
  });
1985
1987
  todoWriteProviderInputSchema = z14.object({
1986
- todos: z14.array(todoItemProviderSchema)
1988
+ todos: z14.array(todoItemProviderSchema).max(20)
1987
1989
  });
1988
1990
  todoWriteOutputSchema = z14.object({
1989
1991
  sessionId: z14.string(),
@@ -1995,7 +1997,8 @@ var init_schema10 = __esm(() => {
1995
1997
  inProgress: z14.number().int().nonnegative(),
1996
1998
  completed: z14.number().int().nonnegative(),
1997
1999
  canceled: z14.number().int().nonnegative()
1998
- })
2000
+ }),
2001
+ warnings: z14.array(z14.string()).optional()
1999
2002
  });
2000
2003
  });
2001
2004
 
@@ -2927,14 +2930,139 @@ ${environmentContext.trim()}` : "");
2927
2930
  var cwdPromptPlaceholder = "{cwd}", defaultCodingAgentSystemPrompt, toolCallingDisciplineAddendum;
2928
2931
  var init_coding_agent_prompt = __esm(() => {
2929
2932
  init_coding_agent_modes();
2930
- defaultCodingAgentSystemPrompt = "You are a coding agent working inside the user's project at this working directory: {cwd}. " + 'You can already see the project \u2014 proactively read it. When the user asks you to review, explain, or work on "my code"/"this", inspect the working directory yourself with list_files, glob_search, grep, and read_file; NEVER ask the user to paste or share code that lives in this directory \u2014 open it. ' + "Use tools for filesystem and codebase tasks instead of guessing, and only interact with files under the working directory. " + "Prefer dedicated tools \u2014 glob_search for paths, grep for text search, structured git tools for repository inspection \u2014 and bash only when those are not enough. " + `Use todo_write to keep multi-step work visible and current, inspect context before risky operations, and emit a brief, natural progress note before major tool actions and after important findings.
2933
+ defaultCodingAgentSystemPrompt = "You are a coding agent working inside the user's project at this working directory: {cwd}. " + 'You can already see the project \u2014 proactively read it. When the user asks you to review, explain, or work on "my code"/"this", inspect the working directory yourself with list_files, glob_search, grep, and read_file; NEVER ask the user to paste or share code that lives in this directory \u2014 open it. ' + "Use tools for filesystem and codebase tasks instead of guessing, and only interact with files under the working directory. " + "Prefer dedicated tools \u2014 glob_search for paths, grep for text search, structured git tools for repository inspection \u2014 and bash only when those are not enough. " + `Inspect context before risky operations. Progress notes: before a major tool action you may emit ONE short note (one sentence, ~15 words) saying what you are about to do. After a tool returns, mention a finding only if it changes your plan, and state it as fact grounded in what the tool actually returned \u2014 cite the file/line/symbol. NEVER claim you found, changed, or fixed something before a tool result confirms it. Skip notes for routine reads, and do not restate the todo list in prose.
2934
+
2935
+ ` + `Task list discipline \u2014 use the todo_write tool to keep a visible, evolving checklist:
2936
+ ` + `- Create the list FIRST. For any task with roughly three or more distinct steps, or any multi-file or multi-stage work, call todo_write as your very first action \u2014 before reading or editing \u2014 with every step listed and all of them set to pending.
2937
+ ` + `- Keep exactly ONE item in_progress at a time. Mark an item in_progress immediately before you start it; never have two items in_progress at once.
2938
+ ` + `- Mark an item completed only after a tool result confirms it is actually done (test passed, file written, build clean) \u2014 not on intent. Do this the MOMENT it is confirmed; do not batch completions or wait until the end of the turn.
2939
+ ` + `- Completions are monotonic: once a task is completed, do not silently move it back to pending or in_progress. Reopen it only if it genuinely regressed.
2940
+ ` + `- Every todo_write call is a FULL REWRITE of the list: always send every task with its current status (re-state the completed and pending ones too \u2014 never send only the changed item). Keep each item terse (a short label, not a paragraph).
2941
+ ` + `- Skip the list for trivial, single-step, or purely conversational requests \u2014 a checklist there is just noise.
2931
2942
 
2932
2943
  ` + `You are an agent \u2014 keep working until the request is fully resolved. Never stop with a partial answer; if you say you will do something, do it in the same turn by calling a tool. Do not finish while any todo is pending or in_progress \u2014 end your turn only when the task is complete or you are blocked on input that only the user can provide.
2933
2944
 
2945
+ ` + `When the task is complete, end with a short summary (a few lines) grounded in the todos you actually completed and the real file diffs \u2014 name the files you edited, do not list steps you did not verify, and do not repeat the todo card verbatim. If anything is incomplete or you are blocked, say so plainly.
2946
+
2934
2947
  ` + "Long-running processes (dev servers, watchers): never run them as plain foreground bash \u2014 the bash tool enforces a timeout and will kill them. Start them detached with output redirected to a log " + '(Windows: start /B cmd /c "npm run dev > dev.log 2>&1"; Unix: nohup npm run dev > dev.log 2>&1 &), then read the log to confirm startup and the actual port (do not assume the default). ' + "Never kill processes or free ports you did not open yourself.";
2935
2948
  toolCallingDisciplineAddendum = "Invoke tools only through the function-calling interface. " + "Never write tool calls as XML or plain text such as <tool_call>, <function_call>, <invoke>, or bare JSON in your message. " + "Tool arguments must be valid JSON matching the tool's schema. " + "Call one tool at a time and wait for its result before deciding the next step.";
2936
2949
  });
2937
2950
 
2951
+ // packages/ai/src/common/coerce-tool-input.ts
2952
+ function coercePrimitive(current, expected) {
2953
+ if (current === undefined || current === null) {
2954
+ return NO_CHANGE;
2955
+ }
2956
+ switch (expected) {
2957
+ case "number": {
2958
+ if (typeof current === "string") {
2959
+ const trimmed = current.trim();
2960
+ if (trimmed !== "" && Number.isFinite(Number(trimmed))) {
2961
+ return Number(trimmed);
2962
+ }
2963
+ }
2964
+ return NO_CHANGE;
2965
+ }
2966
+ case "boolean": {
2967
+ if (typeof current === "string") {
2968
+ const lowered = current.trim().toLowerCase();
2969
+ if (lowered === "true" || lowered === "1" || lowered === "yes") {
2970
+ return true;
2971
+ }
2972
+ if (lowered === "false" || lowered === "0" || lowered === "no") {
2973
+ return false;
2974
+ }
2975
+ }
2976
+ if (current === 1)
2977
+ return true;
2978
+ if (current === 0)
2979
+ return false;
2980
+ return NO_CHANGE;
2981
+ }
2982
+ case "string": {
2983
+ if (typeof current === "number" || typeof current === "boolean") {
2984
+ return String(current);
2985
+ }
2986
+ return NO_CHANGE;
2987
+ }
2988
+ case "object":
2989
+ case "array": {
2990
+ if (typeof current === "string") {
2991
+ try {
2992
+ const parsed = JSON.parse(current);
2993
+ const isArray = Array.isArray(parsed);
2994
+ const matches = expected === "array" ? isArray : parsed !== null && typeof parsed === "object" && !isArray;
2995
+ if (matches) {
2996
+ return parsed;
2997
+ }
2998
+ } catch {}
2999
+ }
3000
+ return NO_CHANGE;
3001
+ }
3002
+ default:
3003
+ return NO_CHANGE;
3004
+ }
3005
+ }
3006
+ function coerceAtPath(root, path4, expected) {
3007
+ if (path4.length === 0 || root === null || typeof root !== "object") {
3008
+ return false;
3009
+ }
3010
+ let parent = root;
3011
+ for (let index = 0;index < path4.length - 1; index += 1) {
3012
+ const next = parent[path4[index]];
3013
+ if (next === null || typeof next !== "object") {
3014
+ return false;
3015
+ }
3016
+ parent = next;
3017
+ }
3018
+ const key = path4[path4.length - 1];
3019
+ const coerced = coercePrimitive(parent[key], expected);
3020
+ if (coerced === NO_CHANGE) {
3021
+ return false;
3022
+ }
3023
+ parent[key] = coerced;
3024
+ return true;
3025
+ }
3026
+ function coerceToolInputToSchema(value, schema) {
3027
+ if (value === null || typeof value !== "object") {
3028
+ return null;
3029
+ }
3030
+ let current;
3031
+ try {
3032
+ current = structuredClone(value);
3033
+ } catch {
3034
+ current = value;
3035
+ }
3036
+ for (let pass = 0;pass < MAX_PASSES; pass += 1) {
3037
+ const result = schema.safeParse(current);
3038
+ if (result.success) {
3039
+ return result.data;
3040
+ }
3041
+ let changed = false;
3042
+ for (const issue of result.error.issues) {
3043
+ if (issue.code !== "invalid_type") {
3044
+ continue;
3045
+ }
3046
+ const expected = issue.expected;
3047
+ if (typeof expected !== "string") {
3048
+ continue;
3049
+ }
3050
+ if (coerceAtPath(current, issue.path, expected)) {
3051
+ changed = true;
3052
+ }
3053
+ }
3054
+ if (!changed) {
3055
+ break;
3056
+ }
3057
+ }
3058
+ const final = schema.safeParse(current);
3059
+ return final.success ? final.data : null;
3060
+ }
3061
+ var NO_CHANGE, MAX_PASSES = 4;
3062
+ var init_coerce_tool_input = __esm(() => {
3063
+ NO_CHANGE = Symbol("no-change");
3064
+ });
3065
+
2938
3066
  // packages/ai/src/providers/tool-call-json-repair.ts
2939
3067
  import { parsePartialJson } from "ai";
2940
3068
  async function repairToolJson(raw) {
@@ -3129,15 +3257,19 @@ async function repairCodingAgentToolCall({
3129
3257
  if (!repairedInput || typeof repairedInput !== "object") {
3130
3258
  return null;
3131
3259
  }
3260
+ const schema = codingToolProviderInputSchemas[toolCall.toolName];
3261
+ const coerced = schema ? coerceToolInputToSchema(repairedInput, schema) : null;
3132
3262
  return {
3133
3263
  ...toolCall,
3134
- input: JSON.stringify(repairedInput)
3264
+ input: JSON.stringify(coerced ?? repairedInput)
3135
3265
  };
3136
3266
  }
3137
3267
  return null;
3138
3268
  }
3139
3269
  var toolNameAliases;
3140
3270
  var init_coding_agent_tool_repair = __esm(() => {
3271
+ init_agent_tools();
3272
+ init_coerce_tool_input();
3141
3273
  init_tool_call_json_repair();
3142
3274
  toolNameAliases = {
3143
3275
  read: "read_file",
@@ -3226,19 +3358,6 @@ function getLastUserText(messages, prompt) {
3226
3358
  }
3227
3359
  return collectTextFromUnknown(prompt).trim();
3228
3360
  }
3229
- function pushUniqueTool(tools, toolName) {
3230
- if (!tools.includes(toolName)) {
3231
- tools.push(toolName);
3232
- }
3233
- }
3234
- function includesAny(text, tokens) {
3235
- return tokens.some((token) => {
3236
- if (token.includes("://") || token.includes(" ")) {
3237
- return text.includes(token);
3238
- }
3239
- return new RegExp(`(^|[^a-z0-9])${token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([^a-z0-9]|$)`).test(text);
3240
- });
3241
- }
3242
3361
  function selectCodingAgentIntentTools({
3243
3362
  mode,
3244
3363
  prompt,
@@ -3249,75 +3368,7 @@ function selectCodingAgentIntentTools({
3249
3368
  if (!normalizedText || casualPromptPattern.test(normalizedText)) {
3250
3369
  return [];
3251
3370
  }
3252
- const situational = [];
3253
- const hasGitIntent = includesAny(normalizedText, [
3254
- "git",
3255
- "diff",
3256
- "status",
3257
- "commit",
3258
- "log",
3259
- "show",
3260
- "revision",
3261
- "changes"
3262
- ]);
3263
- if (hasGitIntent) {
3264
- if (includesAny(normalizedText, ["status", "state", "dirty", "working tree"]) || !includesAny(normalizedText, ["diff", "log", "show", "history", "commit"])) {
3265
- pushUniqueTool(situational, "git_status");
3266
- }
3267
- if (includesAny(normalizedText, ["diff", "changes", "patch"])) {
3268
- pushUniqueTool(situational, "git_diff");
3269
- }
3270
- if (includesAny(normalizedText, ["log", "history", "commits"])) {
3271
- pushUniqueTool(situational, "git_log");
3272
- }
3273
- if (includesAny(normalizedText, ["show", "revision", "commit"])) {
3274
- pushUniqueTool(situational, "git_show");
3275
- }
3276
- }
3277
- if (mode === "build" && includesAny(normalizedText, ["todo", "tasks", "checklist", "steps", "epic", "ticket"])) {
3278
- pushUniqueTool(situational, "todo_write");
3279
- }
3280
- if (includesAny(normalizedText, ["tool_search", "tool search", "find tool", "available tool"])) {
3281
- pushUniqueTool(situational, "tool_search");
3282
- }
3283
- if (includesAny(normalizedText, ["skill", "skills", "load skill"])) {
3284
- pushUniqueTool(situational, "skill");
3285
- }
3286
- if (includesAny(normalizedText, ["mcp", "resource", "resources", "mcp tool", "mcp server"])) {
3287
- pushUniqueTool(situational, "list_mcp_resources");
3288
- if (includesAny(normalizedText, ["read", "resource", "resources"])) {
3289
- pushUniqueTool(situational, "read_mcp_resource");
3290
- }
3291
- if (mode === "build" && includesAny(normalizedText, ["call", "execute", "run"])) {
3292
- pushUniqueTool(situational, "call_mcp_tool");
3293
- }
3294
- }
3295
- if (includesAny(normalizedText, [
3296
- "http://",
3297
- "https://",
3298
- "url",
3299
- "fetch",
3300
- "website",
3301
- "web",
3302
- "online",
3303
- "search internet",
3304
- "latest"
3305
- ])) {
3306
- pushUniqueTool(situational, "web_fetch");
3307
- pushUniqueTool(situational, "web_search");
3308
- }
3309
- if (mode === "plan" && includesAny(normalizedText, ["question", "ask", "clarify"])) {
3310
- pushUniqueTool(situational, "request_user_input");
3311
- }
3312
- const ordered = [...baseReadTools, ...situational];
3313
- if (mode === "build") {
3314
- ordered.push("write_file", "edit_file", "bash");
3315
- }
3316
- const selected = [];
3317
- for (const toolName of ordered) {
3318
- pushUniqueTool(selected, toolName);
3319
- }
3320
- return selected.slice(0, maxProviderActiveTools);
3371
+ return [...getCodingAgentModeDefinition(mode).activeTools];
3321
3372
  }
3322
3373
  function limitProviderActiveTools(tools, maxTools = maxProviderActiveTools) {
3323
3374
  if (tools.length <= maxTools) {
@@ -3326,15 +3377,10 @@ function limitProviderActiveTools(tools, maxTools = maxProviderActiveTools) {
3326
3377
  const toolSet = new Set(tools);
3327
3378
  return providerToolPriority.filter((toolName) => toolSet.has(toolName)).slice(0, maxTools);
3328
3379
  }
3329
- var casualPromptPattern, baseReadTools, maxProviderActiveTools = 7, providerToolPriority;
3380
+ var casualPromptPattern, maxProviderActiveTools = 24, providerToolPriority;
3330
3381
  var init_intent_tool_selection = __esm(() => {
3382
+ init_coding_agent_modes();
3331
3383
  casualPromptPattern = /^(hi|hello|hey|how are you|how r you|what'?s up|whats up|sup|thanks|thank you|ok|okay|yes|no|nice|cool|great)[\s?!.,]*$/i;
3332
- baseReadTools = [
3333
- "list_files",
3334
- "glob_search",
3335
- "read_file",
3336
- "grep"
3337
- ];
3338
3384
  providerToolPriority = [
3339
3385
  "list_files",
3340
3386
  "glob_search",
@@ -4038,6 +4084,16 @@ function getPartState(part) {
4038
4084
  const state = isRecord3(part) ? Reflect.get(part, "state") : undefined;
4039
4085
  return typeof state === "string" ? state : null;
4040
4086
  }
4087
+ function getPlainTextPartContent(part) {
4088
+ if (!isRecord3(part) || Reflect.get(part, "type") !== "text") {
4089
+ return null;
4090
+ }
4091
+ const text = Reflect.get(part, "text");
4092
+ return typeof text === "string" ? text : null;
4093
+ }
4094
+ function messageHasToolPart(message) {
4095
+ return message.parts.some((part) => getToolNameFromPart(part) !== null);
4096
+ }
4041
4097
  function computePruneCutoffIndex(messages, preserveRecentMessages, quantizeUserTurns = DEFAULT_PRUNE_QUANTIZE_USER_TURNS) {
4042
4098
  const candidate = messages.length - preserveRecentMessages;
4043
4099
  if (candidate <= 0) {
@@ -4087,15 +4143,29 @@ function pruneToolOutputs(messages, options) {
4087
4143
  });
4088
4144
  let elidedToolOutputs = 0;
4089
4145
  let dedupedFileReads = 0;
4146
+ let elidedProgressNotes = 0;
4090
4147
  let savedChars = 0;
4091
4148
  const prunedMessages = messages.map((message, messageIndex) => {
4092
4149
  if (messageIndex >= cutoff) {
4093
4150
  return message;
4094
4151
  }
4152
+ const noteCandidate = message.role === "assistant" && messageHasToolPart(message);
4095
4153
  let changed = false;
4096
4154
  const parts = message.parts.map((part, partIndex) => {
4097
4155
  const toolName = getToolNameFromPart(part);
4098
4156
  if (!toolName || getPartState(part) !== "output-available") {
4157
+ if (noteCandidate && toolName === null) {
4158
+ const text = getPlainTextPartContent(part);
4159
+ if (text !== null && text !== PROGRESS_NOTE_ELISION_STUB && text.length > PROGRESS_NOTE_PRUNE_MIN_CHARS) {
4160
+ elidedProgressNotes += 1;
4161
+ savedChars += Math.max(0, text.length - PROGRESS_NOTE_ELISION_STUB.length);
4162
+ changed = true;
4163
+ return {
4164
+ ...part,
4165
+ text: PROGRESS_NOTE_ELISION_STUB
4166
+ };
4167
+ }
4168
+ }
4099
4169
  return part;
4100
4170
  }
4101
4171
  const output = isRecord3(part) ? Reflect.get(part, "output") : undefined;
@@ -4112,7 +4182,7 @@ function pruneToolOutputs(messages, options) {
4112
4182
  }
4113
4183
  const stub = {
4114
4184
  lightcodeElided: true,
4115
- note: isStaleRead ? "Earlier read of this file elided; a later read in this conversation has the current content. Re-run read_file if needed." : "Large tool output elided to preserve context budget. Re-run the tool if this output is needed again.",
4185
+ note: isStaleRead ? "Earlier read of this file elided; its current content is already shown later in this conversation \u2014 do not read it again." : "Large tool output elided to preserve context budget. Re-run the tool only if this output is needed again.",
4116
4186
  originalChars: serializedOutput.length
4117
4187
  };
4118
4188
  if (isStaleRead) {
@@ -4130,10 +4200,11 @@ function pruneToolOutputs(messages, options) {
4130
4200
  messages: prunedMessages,
4131
4201
  elidedToolOutputs,
4132
4202
  dedupedFileReads,
4203
+ elidedProgressNotes,
4133
4204
  savedChars
4134
4205
  };
4135
4206
  }
4136
- var DEFAULT_PRUNE_MIN_OUTPUT_CHARS = 2000, DEFAULT_PRUNE_QUANTIZE_USER_TURNS = 4;
4207
+ var DEFAULT_PRUNE_MIN_OUTPUT_CHARS = 2000, PROGRESS_NOTE_ELISION_STUB = "[progress note elided]", DEFAULT_PRUNE_QUANTIZE_USER_TURNS = 4;
4137
4208
  var init_tier1_prune = __esm(() => {
4138
4209
  init_estimate();
4139
4210
  init_message_parts();
@@ -6050,6 +6121,57 @@ function getExportPath(sessionTitle, sessionId) {
6050
6121
  }
6051
6122
  var init_export_session_markdown = () => {};
6052
6123
 
6124
+ // packages/ai/src/tool-call-repeat-guard.ts
6125
+ function stableStringify(value) {
6126
+ if (value === null || typeof value !== "object") {
6127
+ return JSON.stringify(value) ?? "null";
6128
+ }
6129
+ if (Array.isArray(value)) {
6130
+ return `[${value.map(stableStringify).join(",")}]`;
6131
+ }
6132
+ const record = value;
6133
+ const entries = Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`);
6134
+ return `{${entries.join(",")}}`;
6135
+ }
6136
+ function assertNotRepeatingToolCall(toolName, input, turnKey) {
6137
+ if (!turnKey || !guardedTools.has(toolName)) {
6138
+ return;
6139
+ }
6140
+ const signature = `${toolName}:${stableStringify(input)}`;
6141
+ const previous = lastCallByTurn.get(turnKey);
6142
+ const count = previous && previous.signature === signature ? previous.count + 1 : 1;
6143
+ lastCallByTurn.set(turnKey, { signature, count });
6144
+ if (lastCallByTurn.size > MAX_TURNS_TRACKED) {
6145
+ const oldest = lastCallByTurn.keys().next().value;
6146
+ if (oldest !== undefined) {
6147
+ lastCallByTurn.delete(oldest);
6148
+ }
6149
+ }
6150
+ if (count >= REPEAT_THRESHOLD) {
6151
+ throw new RepeatedToolCallError(`You have called ${toolName} with identical arguments ${count} times in a row and the result has not changed. ` + "Stop repeating it \u2014 use the information already in this conversation and take the next concrete action " + "(make your edit, or call a different tool).");
6152
+ }
6153
+ }
6154
+ var guardedTools, REPEAT_THRESHOLD = 3, MAX_TURNS_TRACKED = 8, RepeatedToolCallError, lastCallByTurn;
6155
+ var init_tool_call_repeat_guard = __esm(() => {
6156
+ guardedTools = new Set([
6157
+ "read_file",
6158
+ "list_files",
6159
+ "glob_search",
6160
+ "grep",
6161
+ "git_status",
6162
+ "git_diff",
6163
+ "git_log",
6164
+ "git_show"
6165
+ ]);
6166
+ RepeatedToolCallError = class RepeatedToolCallError extends Error {
6167
+ constructor(message) {
6168
+ super(message);
6169
+ this.name = "RepeatedToolCallError";
6170
+ }
6171
+ };
6172
+ lastCallByTurn = new Map;
6173
+ });
6174
+
6053
6175
  // packages/ai/src/common/resolve-within-workspace.ts
6054
6176
  import path9 from "path";
6055
6177
  import {
@@ -7280,6 +7402,47 @@ function getTodoFilePath(sessionId, workspaceContext) {
7280
7402
  allowMissing: true
7281
7403
  });
7282
7404
  }
7405
+ function truncateContent(content) {
7406
+ if (content.length <= TODO_CONTENT_MAX_CHARS) {
7407
+ return content;
7408
+ }
7409
+ return `${content.slice(0, TODO_CONTENT_MAX_CHARS - 1).trimEnd()}\u2026`;
7410
+ }
7411
+ function validateTodoInvariants(todos, previousTodos) {
7412
+ const warnings = [];
7413
+ let normalized = todos.map((todo) => {
7414
+ const capped = truncateContent(todo.content);
7415
+ if (capped !== todo.content) {
7416
+ warnings.push(`Todo "${capped}" was longer than ${TODO_CONTENT_MAX_CHARS} characters and was truncated; keep items terse.`);
7417
+ }
7418
+ return capped === todo.content ? todo : { ...todo, content: capped };
7419
+ });
7420
+ const inProgress = normalized.filter((todo) => todo.status === "in_progress");
7421
+ if (inProgress.length > 1) {
7422
+ let seen = false;
7423
+ normalized = normalized.map((todo) => {
7424
+ if (todo.status !== "in_progress") {
7425
+ return todo;
7426
+ }
7427
+ if (!seen) {
7428
+ seen = true;
7429
+ return todo;
7430
+ }
7431
+ return { ...todo, status: "pending" };
7432
+ });
7433
+ warnings.push(`${inProgress.length} tasks were in_progress; kept "${inProgress[0].content}" and reset the rest to pending. Keep exactly one task in_progress.`);
7434
+ } else if (inProgress.length === 0 && normalized.some((todo) => todo.status === "pending")) {
7435
+ warnings.push("No task is in_progress but pending work remains; mark the next task in_progress before starting it.");
7436
+ }
7437
+ const previouslyCompleted = new Map(previousTodos.filter((todo) => todo.status === "completed").map((todo) => [todo.id ?? todo.content, todo]));
7438
+ for (const todo of normalized) {
7439
+ const key = todo.id ?? todo.content;
7440
+ if (previouslyCompleted.has(key) && todo.status !== "completed" && todo.status !== "canceled") {
7441
+ warnings.push(`Task "${todo.content}" was completed earlier but is now ${todo.status}; completions should be monotonic. Reopen only if it genuinely regressed.`);
7442
+ }
7443
+ }
7444
+ return { todos: normalized, warnings };
7445
+ }
7283
7446
  function summarizeTodos(todos) {
7284
7447
  return {
7285
7448
  total: todos.length,
@@ -7307,17 +7470,20 @@ async function executeTodoWrite(input, {
7307
7470
  } = {}) {
7308
7471
  const parsedInput = todoWriteInputSchema.parse(input);
7309
7472
  const todoFilePath = getTodoFilePath(sessionId, workspaceContext);
7310
- const todos = parsedInput.todos.map((todo, index) => todoItemSchema.parse({
7473
+ const submitted = parsedInput.todos.map((todo, index) => todoItemSchema.parse({
7311
7474
  ...todo,
7312
7475
  id: todo.id ?? `todo-${index + 1}`
7313
7476
  }));
7477
+ const previousTodos = await loadSessionTodos({ sessionId, workspaceContext });
7478
+ const { todos, warnings } = validateTodoInvariants(submitted, previousTodos);
7314
7479
  await mkdir(path14.dirname(todoFilePath), { recursive: true });
7315
7480
  await writeFile(todoFilePath, JSON.stringify(todos, null, 2), "utf8");
7316
7481
  return todoWriteOutputSchema.parse({
7317
7482
  sessionId: safeTodoSessionId(sessionId),
7318
7483
  path: toWorkspaceRelativePath(todoFilePath, workspaceContext),
7319
7484
  todos,
7320
- summary: summarizeTodos(todos)
7485
+ summary: summarizeTodos(todos),
7486
+ ...warnings.length > 0 ? { warnings } : {}
7321
7487
  });
7322
7488
  }
7323
7489
  var defaultSessionId = "default";
@@ -7758,12 +7924,22 @@ function assertToolPermission(toolName, input, options) {
7758
7924
  }
7759
7925
  }
7760
7926
  function parseCodingToolInput(toolName, rawInput) {
7761
- return codingToolInputSchemas[toolName].parse(rawInput);
7927
+ const schema6 = codingToolInputSchemas[toolName];
7928
+ const parsed = schema6.safeParse(rawInput);
7929
+ if (parsed.success) {
7930
+ return parsed.data;
7931
+ }
7932
+ const coerced = coerceToolInputToSchema(rawInput, schema6);
7933
+ if (coerced !== null) {
7934
+ return coerced;
7935
+ }
7936
+ return schema6.parse(rawInput);
7762
7937
  }
7763
7938
  async function executeCodingTool(toolName, input, options) {
7764
7939
  const workspaceContext = getWorkspaceContext(options);
7765
7940
  const validatedInput = parseCodingToolInput(toolName, input);
7766
7941
  assertToolPermission(toolName, validatedInput, options);
7942
+ assertNotRepeatingToolCall(toolName, validatedInput, options?.turnKey);
7767
7943
  const runtime4 = codingToolRuntimes[toolName];
7768
7944
  const rawOutput = await runtime4(validatedInput, { workspaceContext, options });
7769
7945
  return codingToolOutputSchemas[toolName].parse(rawOutput);
@@ -7771,6 +7947,8 @@ async function executeCodingTool(toolName, input, options) {
7771
7947
  var codingToolRuntimes;
7772
7948
  var init_runtime_registry = __esm(() => {
7773
7949
  init_agent_tools();
7950
+ init_coerce_tool_input();
7951
+ init_tool_call_repeat_guard();
7774
7952
  init_runtime4();
7775
7953
  init_runtime6();
7776
7954
  init_runtime7();
@@ -8072,6 +8250,7 @@ __export(exports_runtime, {
8072
8250
  WORKSPACE: () => WORKSPACE,
8073
8251
  PermissionPolicy: () => PermissionPolicy,
8074
8252
  PermissionDeniedError: () => PermissionDeniedError,
8253
+ PROGRESS_NOTE_ELISION_STUB: () => PROGRESS_NOTE_ELISION_STUB,
8075
8254
  McpServerManager: () => McpServerManager,
8076
8255
  MAX_TOOL_TEXT_OUTPUT_CHARS: () => MAX_TOOL_TEXT_OUTPUT_CHARS,
8077
8256
  MAX_TOOL_SEARCH_RESULTS: () => MAX_TOOL_SEARCH_RESULTS,
@@ -8236,6 +8415,24 @@ async function runCopyAction({
8236
8415
  const copied = await copyToClipboard(payload);
8237
8416
  notify(copied ? `Copied ${label} to clipboard (${payload.length} chars).` : "Copy failed: clipboard unavailable in this terminal.", copied ? "info" : "error");
8238
8417
  }
8418
+ async function runSkillsAction({
8419
+ notify
8420
+ }) {
8421
+ try {
8422
+ const { listSkills: listSkills2 } = await Promise.resolve().then(() => (init_runtime17(), exports_runtime));
8423
+ const skills = listSkills2({ cwd: process.cwd() });
8424
+ if (skills.length === 0) {
8425
+ notify("No skills found. Add one at .lightcode/skills/<name>/SKILL.md (or ~/.lightcode/skills/ for all projects).");
8426
+ return;
8427
+ }
8428
+ const lines = skills.map((skill) => `\u2022 ${skill.name}${skill.description ? ` \u2014 ${skill.description}` : ""} (${skill.source})`);
8429
+ notify(`Available skills (ask the agent to use one by name):
8430
+ ${lines.join(`
8431
+ `)}`);
8432
+ } catch (error) {
8433
+ notify(`Could not list skills: ${error instanceof Error ? error.message : "unknown error"}`, "error");
8434
+ }
8435
+ }
8239
8436
  function findChatSlashAction(text) {
8240
8437
  const command = text.trim().split(/\s+/)[0]?.toLowerCase() ?? "";
8241
8438
  return chatSlashActions.find((action) => action.shortcut === command) ?? null;
@@ -8294,6 +8491,14 @@ var init_chat_slash_actions = __esm(() => {
8294
8491
  shortcut: "/undo",
8295
8492
  run: runUndoAction
8296
8493
  },
8494
+ {
8495
+ kind: "chat-action",
8496
+ id: "skills",
8497
+ label: "List skills",
8498
+ description: "Show the skills the agent can load in this workspace",
8499
+ shortcut: "/skills",
8500
+ run: runSkillsAction
8501
+ },
8297
8502
  {
8298
8503
  kind: "chat-action",
8299
8504
  id: "permission",
@@ -9298,7 +9503,7 @@ function useCodingSessionChat({
9298
9503
  sendMessage,
9299
9504
  setMessages
9300
9505
  ]);
9301
- const submitInput = useCallback((text) => {
9506
+ const submitInput = useCallback((text, files) => {
9302
9507
  if (canRetryRecoverableResponse && isRetryCommand(text)) {
9303
9508
  retryRecoverableResponse();
9304
9509
  return;
@@ -9322,7 +9527,7 @@ function useCodingSessionChat({
9322
9527
  }
9323
9528
  setToolExecutionError(null);
9324
9529
  advanceTurnKey();
9325
- sendMessage({ text });
9530
+ sendMessage({ text, files });
9326
9531
  }, [
9327
9532
  advanceTurnKey,
9328
9533
  pendingApprovals.length,
@@ -10579,83 +10784,61 @@ function getOutputPreviewLines(part, maxLines) {
10579
10784
  return value.split(/\r?\n/).slice(0, maxLines).map((line) => truncateInline2(line, OUTPUT_LINE_MAX_CHARS));
10580
10785
  }
10581
10786
  }
10582
- const matches = Reflect.get(output, "matches");
10583
- if (Array.isArray(matches)) {
10584
- return [`${matches.length} match${matches.length === 1 ? "" : "es"}`];
10585
- }
10586
10787
  return [];
10587
10788
  }
10588
- function basename(filePath) {
10589
- const parts = filePath.split(/[\\/]/).filter(Boolean);
10590
- return parts[parts.length - 1] ?? filePath;
10591
- }
10592
- function countTextLines(text) {
10593
- const trimmed = text.replace(/[\r\n]+$/, "");
10594
- return trimmed.length === 0 ? 0 : trimmed.split(/\r?\n/).length;
10595
- }
10596
10789
  function countDiffStats(diff) {
10597
10790
  let added = 0;
10598
10791
  let removed = 0;
10599
- for (const line of diff.split(/\r?\n/)) {
10792
+ for (const line of diff.split(`
10793
+ `)) {
10600
10794
  if (line.startsWith("+") && !line.startsWith("+++")) {
10601
- added += 1;
10795
+ added++;
10602
10796
  } else if (line.startsWith("-") && !line.startsWith("---")) {
10603
- removed += 1;
10797
+ removed++;
10604
10798
  }
10605
10799
  }
10606
10800
  return { added, removed };
10607
10801
  }
10608
- function pluralize(count, noun) {
10609
- return `${count} ${noun}${count === 1 ? "" : "s"}`;
10802
+ function basename(path16) {
10803
+ const normalized = path16.replaceAll("\\", "/");
10804
+ const lastSlash = normalized.lastIndexOf("/");
10805
+ return lastSlash === -1 ? normalized : normalized.slice(lastSlash + 1);
10610
10806
  }
10611
10807
  function summarizeToolOutput(part) {
10808
+ if (part.state === "output-error") {
10809
+ return { text: "Failed to execute tool", isError: true };
10810
+ }
10811
+ if (part.state === "output-denied") {
10812
+ return { text: "Execution denied", isError: true };
10813
+ }
10612
10814
  if (part.state !== "output-available") {
10613
10815
  return null;
10614
10816
  }
10615
10817
  const output = part.output;
10616
10818
  if (typeof output === "string") {
10617
- const lines = countTextLines(output);
10618
- return lines > 0 ? { text: pluralize(lines, "line"), isError: false } : null;
10619
- }
10620
- if (!output || typeof output !== "object") {
10621
- return null;
10622
- }
10623
- const exitCode = Reflect.get(output, "exitCode");
10624
- if (typeof exitCode === "number") {
10625
- const stdout = Reflect.get(output, "stdout");
10626
- const stdoutText = typeof stdout === "string" ? stdout : "";
10627
- const testMatch = /(\d+)\s+(passed|failed|passing|failing)/i.exec(stdoutText);
10628
- if (testMatch) {
10629
- return { text: `Tests: ${testMatch[0]}`, isError: exitCode !== 0 };
10819
+ const lines = output.trim().split(/\r?\n/);
10820
+ if (lines.length === 0 || !lines[0]) {
10821
+ return { text: "No output generated" };
10630
10822
  }
10631
- const lines = countTextLines(stdoutText);
10632
- const tail = lines > 0 ? ` \xB7 ${pluralize(lines, "line")}` : "";
10633
- return { text: `exit ${exitCode}${tail}`, isError: exitCode !== 0 };
10634
- }
10635
- for (const key of ["content", "text"]) {
10636
- const value = Reflect.get(output, key);
10637
- if (typeof value === "string" && value.trim()) {
10638
- return { text: pluralize(countTextLines(value), "line"), isError: false };
10823
+ if (lines.length === 1) {
10824
+ return null;
10639
10825
  }
10826
+ return { text: `${lines.length} lines of output` };
10640
10827
  }
10641
- const matches = Reflect.get(output, "matches");
10642
- if (Array.isArray(matches)) {
10643
- return { text: pluralize(matches.length, "match"), isError: false };
10828
+ if (!output || typeof output !== "object") {
10829
+ return { text: "Completed" };
10644
10830
  }
10645
- for (const key of ["paths", "files", "results", "entries"]) {
10646
- const arr = Reflect.get(output, key);
10647
- if (Array.isArray(arr)) {
10648
- const noun = key === "results" || key === "entries" ? "item" : "file";
10649
- return { text: pluralize(arr.length, noun), isError: false };
10650
- }
10831
+ const keys = Object.keys(output).filter((k) => k !== "error");
10832
+ const hasError = Boolean(Reflect.get(output, "error"));
10833
+ if (keys.includes("matches") && Reflect.get(output, "matches") instanceof Array) {
10834
+ const count = Reflect.get(output, "matches").length;
10835
+ return { text: `Found ${count} match${count === 1 ? "" : "es"}`, isError: hasError };
10651
10836
  }
10652
- for (const key of ["summary", "message"]) {
10653
- const value = Reflect.get(output, key);
10654
- if (typeof value === "string" && value.trim()) {
10655
- return { text: truncateInline2(value, 100), isError: false };
10656
- }
10837
+ if (keys.includes("entries") && Reflect.get(output, "entries") instanceof Array) {
10838
+ const count = Reflect.get(output, "entries").length;
10839
+ return { text: `Listed ${count} file${count === 1 ? "" : "s"}`, isError: hasError };
10657
10840
  }
10658
- return null;
10841
+ return { text: "Completed", isError: hasError };
10659
10842
  }
10660
10843
  function ChatMessageToolInvocationPart({
10661
10844
  part,
@@ -10680,9 +10863,19 @@ function ChatMessageToolInvocationPart({
10680
10863
  const hasCollapsedRawOutput = !fileEditDiff && !expandedToolOutput && getOutputPreviewLines(part, 1).length > 0;
10681
10864
  return /* @__PURE__ */ jsxDEV12("box", {
10682
10865
  flexDirection: "column",
10866
+ paddingLeft: 1,
10867
+ paddingRight: 1,
10868
+ paddingBottom: 1,
10869
+ paddingTop: 1,
10870
+ borderStyle: borderStyleFor.chrome,
10871
+ border: ["left"],
10872
+ borderColor: stateColor,
10873
+ backgroundColor: cliTheme.surfaces.inset,
10874
+ gap: fileEditDiff || rawPreviewLines.length > 0 ? 1 : 0,
10683
10875
  children: [
10684
10876
  /* @__PURE__ */ jsxDEV12("text", {
10685
10877
  fg: stateColor,
10878
+ attributes: TextAttributes9.BOLD,
10686
10879
  children: line
10687
10880
  }, undefined, false, undefined, this),
10688
10881
  secondaryParams ? /* @__PURE__ */ jsxDEV12("text", {
@@ -10797,6 +10990,14 @@ function ChatMessage({ message, pendingApprovalIds }) {
10797
10990
  width: "100%",
10798
10991
  flexDirection: "column",
10799
10992
  gap: space.xs,
10993
+ paddingLeft: 1,
10994
+ paddingRight: 1,
10995
+ paddingTop: 1,
10996
+ paddingBottom: 1,
10997
+ borderStyle: borderStyleFor.chrome,
10998
+ border: ["left"],
10999
+ borderColor: roleStyle.borderColor,
11000
+ backgroundColor: roleStyle.backgroundColor,
10800
11001
  children: [
10801
11002
  /* @__PURE__ */ jsxDEV13("text", {
10802
11003
  fg: roleStyle.labelColor,
@@ -10806,8 +11007,8 @@ function ChatMessage({ message, pendingApprovalIds }) {
10806
11007
  /* @__PURE__ */ jsxDEV13("box", {
10807
11008
  width: "100%",
10808
11009
  flexDirection: "column",
10809
- gap: space.xs,
10810
- paddingLeft: space.md,
11010
+ gap: space.md,
11011
+ paddingTop: space.xs,
10811
11012
  children: renderedParts
10812
11013
  }, undefined, false, undefined, this)
10813
11014
  ]
@@ -10872,7 +11073,7 @@ function ChatShell({
10872
11073
  /* @__PURE__ */ jsxDEV14("box", {
10873
11074
  width: "100%",
10874
11075
  flexGrow: 1,
10875
- borderStyle: "single",
11076
+ borderStyle: borderStyleFor.card,
10876
11077
  borderColor: cliTheme.borders.default,
10877
11078
  backgroundColor: cliTheme.surfaces.inset,
10878
11079
  children: /* @__PURE__ */ jsxDEV14("scrollbox", {
@@ -11131,6 +11332,74 @@ var init_permission_mode_selector = __esm(() => {
11131
11332
  ];
11132
11333
  });
11133
11334
 
11335
+ // apps/cli/src/utils/clipboard-image.ts
11336
+ import { spawn } from "child_process";
11337
+ import { mkdtemp, readFile as readFile3, rm } from "fs/promises";
11338
+ import { tmpdir } from "os";
11339
+ import path16 from "path";
11340
+ async function readClipboardImage() {
11341
+ if (process.platform !== "darwin") {
11342
+ return null;
11343
+ }
11344
+ let dir = null;
11345
+ try {
11346
+ dir = await mkdtemp(path16.join(tmpdir(), "lightcode-clip-"));
11347
+ const file = path16.join(dir, "clipboard.png");
11348
+ const wrote = await writeClipboardPngViaOsascript(file);
11349
+ if (!wrote) {
11350
+ return null;
11351
+ }
11352
+ const bytes = await readFile3(file);
11353
+ if (bytes.byteLength === 0) {
11354
+ return null;
11355
+ }
11356
+ return {
11357
+ mediaType: "image/png",
11358
+ base64: bytes.toString("base64"),
11359
+ byteLength: bytes.byteLength
11360
+ };
11361
+ } catch {
11362
+ return null;
11363
+ } finally {
11364
+ if (dir) {
11365
+ await rm(dir, { recursive: true, force: true }).catch(() => {});
11366
+ }
11367
+ }
11368
+ }
11369
+ function writeClipboardPngViaOsascript(filePath) {
11370
+ const script = [
11371
+ "try",
11372
+ " set thePng to (the clipboard as \xABclass PNGf\xBB)",
11373
+ ` set fp to open for access POSIX file ${JSON.stringify(filePath)} with write permission`,
11374
+ " set eof fp to 0",
11375
+ " write thePng to fp",
11376
+ " close access fp",
11377
+ ' return "OK"',
11378
+ "on error",
11379
+ " try",
11380
+ " close access fp",
11381
+ " end try",
11382
+ ' return "NONE"',
11383
+ "end try"
11384
+ ];
11385
+ const args = [];
11386
+ for (const line of script) {
11387
+ args.push("-e", line);
11388
+ }
11389
+ return new Promise((resolve) => {
11390
+ let stdout = "";
11391
+ const child = spawn("osascript", args, {
11392
+ stdio: ["ignore", "pipe", "ignore"]
11393
+ });
11394
+ child.stdout?.on("data", (chunk) => {
11395
+ stdout += chunk.toString();
11396
+ });
11397
+ child.on("error", () => resolve(false));
11398
+ child.on("close", () => resolve(stdout.trim() === "OK"));
11399
+ });
11400
+ }
11401
+ var init_clipboard_image = () => {};
11402
+
11134
11403
  // apps/cli/src/utils/fuzzy-match.ts
11135
11404
  function fuzzyScore(query, candidate) {
11136
11405
  const normalizedQuery = query.toLowerCase();
@@ -11166,10 +11435,68 @@ var init_fuzzy_match = __esm(() => {
11166
11435
  BOUNDARY_CHARS = new Set(["/", "\\", ".", "-", "_"]);
11167
11436
  });
11168
11437
 
11438
+ // apps/cli/src/utils/image-attachments.ts
11439
+ function formatImageChip(index) {
11440
+ return `[Image #${index}]`;
11441
+ }
11442
+ function clipboardImageToFilePart(index, image) {
11443
+ return {
11444
+ type: "file",
11445
+ mediaType: image.mediaType,
11446
+ filename: `pasted-image-${index}.png`,
11447
+ url: `data:${image.mediaType};base64,${image.base64}`
11448
+ };
11449
+ }
11450
+ function stripImageChips(text) {
11451
+ return text.replace(IMAGE_CHIP_PATTERN, "").replace(/ {2,}/g, " ");
11452
+ }
11453
+ var IMAGE_CHIP_PATTERN;
11454
+ var init_image_attachments = __esm(() => {
11455
+ IMAGE_CHIP_PATTERN = /\[Image #(\d+)\]/g;
11456
+ });
11457
+
11458
+ // apps/cli/src/utils/paste-placeholders.ts
11459
+ function countLines(text) {
11460
+ if (text.length === 0) {
11461
+ return 0;
11462
+ }
11463
+ return text.split(/\r\n|\r|\n/).length;
11464
+ }
11465
+ function shouldCollapsePaste(text) {
11466
+ return countLines(text) > PASTE_PLACEHOLDER_LINE_THRESHOLD || text.length > PASTE_PLACEHOLDER_CHAR_THRESHOLD;
11467
+ }
11468
+ function formatPastePlaceholder(index, text) {
11469
+ return `[Pasted text #${index} +${countLines(text)} lines]`;
11470
+ }
11471
+ function expandPastePlaceholders(text, store) {
11472
+ return text.replace(PASTE_PLACEHOLDER_PATTERN, (match, rawIndex) => {
11473
+ const stored = store.get(Number(rawIndex));
11474
+ return stored ?? match;
11475
+ });
11476
+ }
11477
+ var PASTE_PLACEHOLDER_LINE_THRESHOLD = 5, PASTE_PLACEHOLDER_CHAR_THRESHOLD = 400, PASTE_PLACEHOLDER_PATTERN;
11478
+ var init_paste_placeholders = __esm(() => {
11479
+ PASTE_PLACEHOLDER_PATTERN = /\[Pasted text #(\d+) \+\d+ lines\]/g;
11480
+ });
11481
+
11169
11482
  // apps/cli/src/components/chat/chat-text-area.tsx
11170
- import { TextAttributes as TextAttributes13 } from "@opentui/core";
11483
+ import {
11484
+ decodePasteBytes,
11485
+ stripAnsiSequences,
11486
+ TextAttributes as TextAttributes13
11487
+ } from "@opentui/core";
11488
+ import { appendFileSync as appendFileSync2 } from "fs";
11171
11489
  import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef2, useState as useState5 } from "react";
11172
11490
  import { jsxDEV as jsxDEV16 } from "@opentui/react/jsx-dev-runtime";
11491
+ function pasteDebug(message) {
11492
+ if (!process.env.LIGHTCODE_PASTE_DEBUG) {
11493
+ return;
11494
+ }
11495
+ try {
11496
+ appendFileSync2("/tmp/lightcode-paste-debug.log", `${message}
11497
+ `);
11498
+ } catch {}
11499
+ }
11173
11500
  function getTrailingMentionQuery(text) {
11174
11501
  const match = text.match(trailingMentionPattern);
11175
11502
  return match ? match[1] : null;
@@ -11191,9 +11518,66 @@ function ChatTextArea({
11191
11518
  const textareaRef = useRef2(null);
11192
11519
  const lastManualNewlineAt = useRef2(0);
11193
11520
  const wasSlashMenuOpen = useRef2(false);
11521
+ const pasteStore = useRef2(new Map);
11522
+ const pasteCounter = useRef2(0);
11523
+ const imageStore = useRef2(new Map);
11524
+ const imageCounter = useRef2(0);
11194
11525
  const [mentionQuery, setMentionQuery] = useState5(null);
11195
11526
  const [mentionSelected, setMentionSelected] = useState5(0);
11196
11527
  const isFocused = focused && !disabled;
11528
+ const attachClipboardImage = useCallback3(() => {
11529
+ readClipboardImage().then((image) => {
11530
+ pasteDebug(`clipboard read: ${image ? `image ${image.byteLength} bytes` : "no image"}`);
11531
+ const textarea = textareaRef.current;
11532
+ if (!image || !textarea) {
11533
+ return;
11534
+ }
11535
+ const index = imageCounter.current + 1;
11536
+ imageCounter.current = index;
11537
+ imageStore.current.set(index, clipboardImageToFilePart(index, image));
11538
+ textarea.insertText(formatImageChip(index));
11539
+ onTextChange?.(textarea.plainText);
11540
+ });
11541
+ }, [onTextChange]);
11542
+ const handlePaste = useCallback3((event) => {
11543
+ const pasted = stripAnsiSequences(decodePasteBytes(event.bytes));
11544
+ pasteDebug(`paste fired: bytes=${event.bytes?.length ?? "?"} decodedLen=${pasted.length} empty=${pasted.trim().length === 0}`);
11545
+ const textarea = textareaRef.current;
11546
+ if (!textarea) {
11547
+ return;
11548
+ }
11549
+ if (pasted.trim().length === 0) {
11550
+ event.preventDefault();
11551
+ attachClipboardImage();
11552
+ return;
11553
+ }
11554
+ if (!shouldCollapsePaste(pasted)) {
11555
+ return;
11556
+ }
11557
+ event.preventDefault();
11558
+ const index = pasteCounter.current + 1;
11559
+ pasteCounter.current = index;
11560
+ pasteStore.current.set(index, pasted);
11561
+ textarea.insertText(formatPastePlaceholder(index, pasted));
11562
+ const next = textarea.plainText;
11563
+ if (mentionCandidates) {
11564
+ setMentionQuery(getTrailingMentionQuery(next));
11565
+ setMentionSelected(0);
11566
+ }
11567
+ onTextChange?.(next);
11568
+ }, [mentionCandidates, onTextChange]);
11569
+ useEffect3(() => {
11570
+ const textarea = textareaRef.current;
11571
+ if (!textarea) {
11572
+ return;
11573
+ }
11574
+ textarea.onPaste = handlePaste;
11575
+ return () => {
11576
+ if (textarea.onPaste === handlePaste) {
11577
+ textarea.onPaste = undefined;
11578
+ }
11579
+ };
11580
+ }, [handlePaste]);
11197
11581
  const mentionMatches = mentionQuery !== null && mentionCandidates && mentionCandidates.length > 0 ? fuzzyFilter(mentionQuery, mentionCandidates) : [];
11198
11582
  const selectedMentionIndex = Math.min(mentionSelected, Math.max(mentionMatches.length - 1, 0));
11199
11583
  const acceptMention = useCallback3((mentionPath) => {
@@ -11217,6 +11601,12 @@ function ChatTextArea({
11217
11601
  textareaRef.current?.newLine();
11218
11602
  return;
11219
11603
  }
11604
+ if (event.name === "v" && event.ctrl) {
11605
+ event.preventDefault();
11606
+ pasteDebug("ctrl+v: grab clipboard image");
11607
+ attachClipboardImage();
11608
+ return;
11609
+ }
11220
11610
  if (mentionMatches.length === 0) {
11221
11611
  return;
11222
11612
  }
@@ -11234,18 +11624,24 @@ function ChatTextArea({
11234
11624
  setMentionQuery(null);
11235
11625
  setMentionSelected(0);
11236
11626
  }
11237
- }, [acceptMention, mentionMatches, selectedMentionIndex]);
11627
+ }, [acceptMention, attachClipboardImage, mentionMatches, selectedMentionIndex]);
11238
11628
  const handleSubmit = useCallback3(() => {
11239
11629
  if (Date.now() - lastManualNewlineAt.current < 100 || disabled) {
11240
11630
  return;
11241
11631
  }
11242
11632
  const rawText = textareaRef.current?.plainText ?? "";
11243
- const submittedText = trimOnSubmit ? rawText.trim() : rawText;
11244
- if (!allowEmpty && submittedText.length === 0) {
11633
+ const expandedText = stripImageChips(expandPastePlaceholders(rawText, pasteStore.current));
11634
+ const submittedText = trimOnSubmit ? expandedText.trim() : expandedText;
11635
+ const files = [...imageStore.current.values()];
11636
+ if (!allowEmpty && submittedText.length === 0 && files.length === 0) {
11245
11637
  return;
11246
11638
  }
11247
- onSubmit(submittedText);
11639
+ onSubmit(submittedText, files.length > 0 ? files : undefined);
11248
11640
  textareaRef.current?.setText("");
11641
+ pasteStore.current.clear();
11642
+ pasteCounter.current = 0;
11643
+ imageStore.current.clear();
11644
+ imageCounter.current = 0;
11249
11645
  setMentionQuery(null);
11250
11646
  setMentionSelected(0);
11251
11647
  onTextChange?.("");
@@ -11280,7 +11676,7 @@ function ChatTextArea({
11280
11676
  beforeInput,
11281
11677
  mentionMatches.length > 0 ? /* @__PURE__ */ jsxDEV16("box", {
11282
11678
  flexDirection: "column",
11283
- borderStyle: "single",
11679
+ borderStyle: borderStyleFor.card,
11284
11680
  borderColor: cliTheme.overlay.border,
11285
11681
  backgroundColor: cliTheme.overlay.surface,
11286
11682
  paddingX: 1,
@@ -11300,7 +11696,7 @@ function ChatTextArea({
11300
11696
  }, undefined, true, undefined, this) : null,
11301
11697
  /* @__PURE__ */ jsxDEV16("box", {
11302
11698
  flexDirection: "column",
11303
- borderStyle: "single",
11699
+ borderStyle: borderStyleFor.card,
11304
11700
  borderColor: isFocused ? cliTheme.input.focusedBorder : cliTheme.input.blurredBorder,
11305
11701
  backgroundColor: cliTheme.input.container,
11306
11702
  paddingX: 1,
@@ -11335,7 +11731,10 @@ function ChatTextArea({
11335
11731
  var trailingMentionPattern, textareaKeyBindings;
11336
11732
  var init_chat_text_area = __esm(() => {
11337
11733
  init_cli_theme();
11734
+ init_clipboard_image();
11338
11735
  init_fuzzy_match();
11736
+ init_image_attachments();
11737
+ init_paste_placeholders();
11339
11738
  trailingMentionPattern = /(?:^|\s)@([^\s@]*)$/;
11340
11739
  textareaKeyBindings = [
11341
11740
  { name: "return", ctrl: true, action: "newline" },
@@ -11366,6 +11765,8 @@ function ChatTodoStatusCard({ todos }) {
11366
11765
  return null;
11367
11766
  }
11368
11767
  const completedCount = todos.filter((todo) => todo.status === "completed").length;
11768
+ const percent = todos.length > 0 ? Math.round(completedCount / todos.length * 100) : 0;
11769
+ const progressText = completedCount === 0 ? "Not started" : `${completedCount}/${todos.length} done (${percent}%)`;
11369
11770
  return /* @__PURE__ */ jsxDEV17("box", {
11370
11771
  width: "100%",
11371
11772
  flexDirection: "column",
@@ -11388,47 +11789,43 @@ function ChatTodoStatusCard({ todos }) {
11388
11789
  }, undefined, false, undefined, this),
11389
11790
  /* @__PURE__ */ jsxDEV17("text", {
11390
11791
  fg: cliTheme.text.muted,
11391
- children: [
11392
- completedCount,
11393
- "/",
11394
- todos.length,
11395
- " done"
11396
- ]
11397
- }, undefined, true, undefined, this)
11792
+ children: progressText
11793
+ }, undefined, false, undefined, this)
11398
11794
  ]
11399
11795
  }, undefined, true, undefined, this),
11400
11796
  /* @__PURE__ */ jsxDEV17("box", {
11401
11797
  width: "100%",
11402
11798
  flexDirection: "column",
11403
- children: todos.slice(0, 6).map((todo, index) => /* @__PURE__ */ jsxDEV17("box", {
11404
- width: "100%",
11405
- flexDirection: "row",
11406
- gap: 1,
11407
- children: [
11408
- /* @__PURE__ */ jsxDEV17("text", {
11409
- fg: getStatusColor(todo.status),
11410
- children: todo.status === "completed" ? "[x]" : "[ ]"
11411
- }, undefined, false, undefined, this),
11412
- /* @__PURE__ */ jsxDEV17("text", {
11413
- fg: cliTheme.text.primary,
11414
- children: truncateInline2(todo.content)
11415
- }, undefined, false, undefined, this),
11416
- /* @__PURE__ */ jsxDEV17("text", {
11417
- fg: getStatusColor(todo.status),
11418
- attributes: TextAttributes14.DIM,
11419
- children: todoStatusLabel[todo.status]
11420
- }, undefined, false, undefined, this)
11421
- ]
11422
- }, todo.id ?? `${todo.content}-${index}`, true, undefined, this))
11423
- }, undefined, false, undefined, this),
11424
- todos.length > 6 ? /* @__PURE__ */ jsxDEV17("text", {
11425
- fg: cliTheme.text.muted,
11426
- attributes: TextAttributes14.DIM,
11427
- children: [
11428
- todos.length - 6,
11429
- " more tasks hidden"
11430
- ]
11431
- }, undefined, true, undefined, this) : null
11799
+ children: todos.map((todo, index) => {
11800
+ const isActive = todo.status === "in_progress";
11801
+ const label = isActive && todo.activeForm ? todo.activeForm : todo.content;
11802
+ return /* @__PURE__ */ jsxDEV17("box", {
11803
+ width: "100%",
11804
+ flexDirection: "row",
11805
+ gap: 1,
11806
+ children: [
11807
+ /* @__PURE__ */ jsxDEV17("text", {
11808
+ fg: getStatusColor(todo.status),
11809
+ children: todo.status === "completed" ? "[x]" : "[ ]"
11810
+ }, undefined, false, undefined, this),
11811
+ /* @__PURE__ */ jsxDEV17("text", {
11812
+ fg: cliTheme.text.primary,
11813
+ children: truncateInline2(label)
11814
+ }, undefined, false, undefined, this),
11815
+ isActive && todo.activeForm ? /* @__PURE__ */ jsxDEV17("text", {
11816
+ fg: cliTheme.text.muted,
11817
+ attributes: TextAttributes14.DIM,
11818
+ children: truncateInline2(todo.content)
11819
+ }, undefined, false, undefined, this) : null,
11820
+ /* @__PURE__ */ jsxDEV17("text", {
11821
+ fg: getStatusColor(todo.status),
11822
+ attributes: TextAttributes14.DIM,
11823
+ children: todoStatusLabel[todo.status]
11824
+ }, undefined, false, undefined, this)
11825
+ ]
11826
+ }, todo.id ?? `${todo.content}-${index}`, true, undefined, this);
11827
+ })
11828
+ }, undefined, false, undefined, this)
11432
11829
  ]
11433
11830
  }, undefined, true, undefined, this);
11434
11831
  }
@@ -11903,7 +12300,7 @@ var init_chat_context_utils = __esm(() => {
11903
12300
 
11904
12301
  // apps/cli/src/utils/file-mentions.ts
11905
12302
  import { promises as fs6 } from "fs";
11906
- import path16 from "path";
12303
+ import path17 from "path";
11907
12304
  function extractFileMentions(text) {
11908
12305
  const mentions = new Set;
11909
12306
  for (const match of text.matchAll(mentionTokenPattern)) {
@@ -11925,8 +12322,8 @@ async function appendMentionAttachments(text, cwd) {
11925
12322
  }
11926
12323
  const attachmentBlocks = [];
11927
12324
  for (const mentionPath of mentionPaths) {
11928
- const absolutePath = path16.resolve(cwd, mentionPath);
11929
- if (!absolutePath.startsWith(path16.resolve(cwd))) {
12325
+ const absolutePath = path17.resolve(cwd, mentionPath);
12326
+ if (!absolutePath.startsWith(path17.resolve(cwd))) {
11930
12327
  continue;
11931
12328
  }
11932
12329
  try {
@@ -12379,24 +12776,26 @@ function ChatScreen() {
12379
12776
  setSlashMenuQuery,
12380
12777
  setSlashMenuSelected
12381
12778
  ]);
12382
- const submitChatInput = useCallback4((text) => {
12383
- const action = findChatSlashAction(text);
12384
- if (action) {
12385
- closeSlashMenu();
12386
- if (action.id === "permission") {
12387
- setPermissionSelectorOpen(true);
12779
+ const submitChatInput = useCallback4((text, files) => {
12780
+ if (!files || files.length === 0) {
12781
+ const action = findChatSlashAction(text);
12782
+ if (action) {
12783
+ closeSlashMenu();
12784
+ if (action.id === "permission") {
12785
+ setPermissionSelectorOpen(true);
12786
+ return;
12787
+ }
12788
+ runChatSlashAction(action, parseChatSlashArgs(text));
12789
+ return;
12790
+ }
12791
+ if (navigateIfSlashRoute(text)) {
12388
12792
  return;
12389
12793
  }
12390
- runChatSlashAction(action, parseChatSlashArgs(text));
12391
- return;
12392
- }
12393
- if (navigateIfSlashRoute(text)) {
12394
- return;
12395
12794
  }
12396
12795
  setActionNotice(null);
12397
12796
  (async () => {
12398
12797
  const expandedText = await appendMentionAttachments(text, process.cwd());
12399
- submitInput(expandedText);
12798
+ submitInput(expandedText, files);
12400
12799
  })();
12401
12800
  }, [closeSlashMenu, navigateIfSlashRoute, runChatSlashAction, submitInput]);
12402
12801
  const modeDefinition = codingAgentModes[mode];
@@ -14102,13 +14501,13 @@ import { TextAttributes as TextAttributes21 } from "@opentui/core";
14102
14501
  import { useKeyboard as useKeyboard9 } from "@opentui/react";
14103
14502
  import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
14104
14503
  import os5 from "os";
14105
- import path17 from "path";
14504
+ import path18 from "path";
14106
14505
  import { useCallback as useCallback7, useState as useState18 } from "react";
14107
14506
  import { useNavigate as useNavigate4 } from "react-router";
14108
14507
  import { jsxDEV as jsxDEV31 } from "@opentui/react/jsx-dev-runtime";
14109
14508
  function getUserSettingsPath() {
14110
14509
  const configured = process.env.LIGHTCODE_USER_CONFIG?.trim();
14111
- return configured ?? path17.join(os5.homedir(), ".lightcode", "settings.json");
14510
+ return configured ?? path18.join(os5.homedir(), ".lightcode", "settings.json");
14112
14511
  }
14113
14512
  function mergeUserSettings(update) {
14114
14513
  const settingsPath = getUserSettingsPath();
@@ -14121,7 +14520,7 @@ function mergeUserSettings(update) {
14121
14520
  }
14122
14521
  } catch {}
14123
14522
  }
14124
- mkdirSync5(path17.dirname(settingsPath), { recursive: true });
14523
+ mkdirSync5(path18.dirname(settingsPath), { recursive: true });
14125
14524
  writeFileSync3(settingsPath, `${JSON.stringify({ ...existing, ...update }, null, 2)}
14126
14525
  `, "utf8");
14127
14526
  return settingsPath;
@@ -15028,9 +15427,9 @@ function AppContent() {
15028
15427
  const inChatSession = location.pathname.startsWith("/sessions/");
15029
15428
  const inputHostsSlashMenu = location.pathname === "/" || location.pathname === "/home" || inChatSession;
15030
15429
  const handleAction = (action) => {
15031
- const path18 = getPathFromAction(action);
15032
- if (path18) {
15033
- navigate(path18);
15430
+ const path19 = getPathFromAction(action);
15431
+ if (path19) {
15432
+ navigate(path19);
15034
15433
  return;
15035
15434
  }
15036
15435
  switch (action) {
@@ -15186,11 +15585,11 @@ function AppContent() {
15186
15585
  /* @__PURE__ */ jsxDEV34("box", {
15187
15586
  flexDirection: "row",
15188
15587
  justifyContent: "space-between",
15189
- paddingLeft: 1,
15190
- paddingRight: 1,
15191
- borderStyle: "single",
15192
- border: ["bottom"],
15193
- borderColor: cliTheme.borders.default,
15588
+ paddingLeft: 2,
15589
+ paddingRight: 2,
15590
+ paddingTop: 1,
15591
+ paddingBottom: 1,
15592
+ backgroundColor: cliTheme.surfaces.panel,
15194
15593
  children: [
15195
15594
  /* @__PURE__ */ jsxDEV34("box", {
15196
15595
  flexDirection: "row",
@@ -15337,11 +15736,9 @@ function AppContent() {
15337
15736
  flexDirection: "row",
15338
15737
  justifyContent: "center",
15339
15738
  gap: 2,
15340
- paddingTop: 1,
15341
- paddingBottom: 1,
15342
- borderStyle: "single",
15343
- border: ["top"],
15344
- borderColor: cliTheme.borders.default,
15739
+ paddingX: 2,
15740
+ paddingY: 1,
15741
+ backgroundColor: cliTheme.surfaces.panel,
15345
15742
  children: /* @__PURE__ */ jsxDEV34("text", {
15346
15743
  fg: ctrlCNotice ? cliTheme.semantic.warning : cliTheme.text.muted,
15347
15744
  children: ctrlCNotice ?? getFooterStatus()