kimiflare 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -16,12 +16,25 @@ function configPath() {
16
16
  const xdg = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
17
17
  return join(xdg, "kimiflare", "config.json");
18
18
  }
19
+ function readReasoningEffortEnv() {
20
+ const raw = process.env.KIMI_REASONING_EFFORT?.toLowerCase();
21
+ if (raw === "low" || raw === "medium" || raw === "high") return raw;
22
+ return void 0;
23
+ }
19
24
  async function loadConfig() {
20
25
  const envAccount = process.env.CLOUDFLARE_ACCOUNT_ID ?? process.env.CF_ACCOUNT_ID;
21
26
  const envToken = process.env.CLOUDFLARE_API_TOKEN ?? process.env.CF_API_TOKEN;
22
27
  const envModel = process.env.KIMI_MODEL ?? DEFAULT_MODEL;
28
+ const envEffort = readReasoningEffortEnv();
29
+ const envTheme = process.env.KIMI_THEME;
23
30
  if (envAccount && envToken) {
24
- return { accountId: envAccount, apiToken: envToken, model: envModel };
31
+ return {
32
+ accountId: envAccount,
33
+ apiToken: envToken,
34
+ model: envModel,
35
+ theme: envTheme,
36
+ reasoningEffort: envEffort
37
+ };
25
38
  }
26
39
  try {
27
40
  const raw = await readFile(configPath(), "utf8");
@@ -30,7 +43,9 @@ async function loadConfig() {
30
43
  return {
31
44
  accountId: envAccount ?? parsed.accountId,
32
45
  apiToken: envToken ?? parsed.apiToken,
33
- model: envModel ?? parsed.model ?? DEFAULT_MODEL
46
+ model: envModel ?? parsed.model ?? DEFAULT_MODEL,
47
+ theme: envTheme ?? parsed.theme,
48
+ reasoningEffort: envEffort ?? parsed.reasoningEffort
34
49
  };
35
50
  }
36
51
  } catch {
@@ -44,11 +59,12 @@ async function saveConfig(cfg) {
44
59
  await chmod(p, 384);
45
60
  return p;
46
61
  }
47
- var DEFAULT_MODEL;
62
+ var DEFAULT_MODEL, DEFAULT_REASONING_EFFORT;
48
63
  var init_config = __esm({
49
64
  "src/config.ts"() {
50
65
  "use strict";
51
66
  DEFAULT_MODEL = "@cf/moonshotai/kimi-k2.6";
67
+ DEFAULT_REASONING_EFFORT = "medium";
52
68
  }
53
69
  });
54
70
 
@@ -122,6 +138,9 @@ async function* runKimi(opts2) {
122
138
  temperature: opts2.temperature ?? 0.2,
123
139
  max_completion_tokens: opts2.maxCompletionTokens ?? 16384
124
140
  };
141
+ if (opts2.reasoningEffort) {
142
+ body.reasoning_effort = opts2.reasoningEffort;
143
+ }
125
144
  for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
126
145
  const res = await fetch(url, {
127
146
  method: "POST",
@@ -289,7 +308,8 @@ async function runAgentTurn(opts2) {
289
308
  tools: toolDefs,
290
309
  signal: opts2.signal,
291
310
  temperature: opts2.temperature,
292
- maxCompletionTokens: opts2.maxCompletionTokens
311
+ maxCompletionTokens: opts2.maxCompletionTokens,
312
+ reasoningEffort: opts2.reasoningEffort
293
313
  });
294
314
  for await (const ev of events) {
295
315
  switch (ev.type) {
@@ -338,7 +358,7 @@ async function runAgentTurn(opts2) {
338
358
  const result = await opts2.executor.run(
339
359
  { id: tc.id, name: tc.function.name, arguments: tc.function.arguments },
340
360
  opts2.callbacks.askPermission,
341
- { cwd: opts2.cwd, signal: opts2.signal }
361
+ { cwd: opts2.cwd, signal: opts2.signal, onTasks: opts2.callbacks.onTasks }
342
362
  );
343
363
  opts2.messages.push({
344
364
  role: "tool",
@@ -359,6 +379,32 @@ var init_loop = __esm({
359
379
  }
360
380
  });
361
381
 
382
+ // src/mode.ts
383
+ function nextMode(m) {
384
+ const i = MODES.indexOf(m);
385
+ return MODES[(i + 1) % MODES.length];
386
+ }
387
+ function isBlockedInPlanMode(toolName) {
388
+ return MUTATING_TOOLS.has(toolName);
389
+ }
390
+ function systemPromptForMode(m) {
391
+ if (m === "plan") {
392
+ return "\n\nPLAN MODE is active. The user wants you to investigate and produce a plan WITHOUT making any changes. Do not call write, edit, or bash. Only use read/glob/grep/web-fetch. At the end, present a concise plan (bullets, files to change, approach). The user will review and then exit plan mode to execute.";
393
+ }
394
+ if (m === "auto") {
395
+ return "\n\nAUTO MODE is active. The user has opted into autonomous execution \u2014 every tool call will be auto-approved. Work efficiently, but do not take irreversible destructive actions (rm -rf, git push --force, dropping tables, etc.) without pausing to describe them in chat first. Prefer smaller reversible steps.";
396
+ }
397
+ return "";
398
+ }
399
+ var MODES, MUTATING_TOOLS;
400
+ var init_mode = __esm({
401
+ "src/mode.ts"() {
402
+ "use strict";
403
+ MODES = ["edit", "plan", "auto"];
404
+ MUTATING_TOOLS = /* @__PURE__ */ new Set(["write", "edit", "bash"]);
405
+ }
406
+ });
407
+
362
408
  // src/agent/system-prompt.ts
363
409
  import { platform, release, homedir as homedir2 } from "os";
364
410
  import { basename } from "path";
@@ -386,15 +432,17 @@ How to work:
386
432
  - Prefer calling tools over guessing. Read files before editing them. Use \`glob\` and \`grep\` to explore code before assuming structure.
387
433
  - Before any mutating tool call (write, edit, bash), state in one short sentence what you're about to do, then call the tool. The user will be asked to approve each mutating call.
388
434
  - When the user asks for a change, make the change. Do not paste code in chat that you could apply with \`edit\` or \`write\`.
435
+ - For multi-step work, call \`tasks_set\` at the start with a short task list (one task "in_progress", the rest "pending"), then call it again after each step completes (flip that one to "completed" and the next to "in_progress"). Skip it for trivial single-step requests.
389
436
  - Keep responses terse. The user sees tool calls and their results inline \u2014 do not re-summarize them unless asked.
390
437
  - If a tool returns an error, read it carefully and adjust; do not retry the same call blindly.
391
438
  - You have a 262k-token context window. Read as much of a file as needed rather than guessing.
392
439
  - If a request is ambiguous, ask one focused question instead of making large assumptions.
393
- - When you finish a task, stop. Do not add a closing summary.`;
440
+ - When you finish a task, stop. Do not add a closing summary.${opts2.mode ? systemPromptForMode(opts2.mode) : ""}`;
394
441
  }
395
442
  var init_system_prompt = __esm({
396
443
  "src/agent/system-prompt.ts"() {
397
444
  "use strict";
445
+ init_mode();
398
446
  }
399
447
  });
400
448
 
@@ -812,6 +860,88 @@ ${bounded}`, MAX_OUTPUT);
812
860
  }
813
861
  });
814
862
 
863
+ // src/tasks-state.ts
864
+ function isValidStatus(s) {
865
+ return s === "pending" || s === "in_progress" || s === "completed";
866
+ }
867
+ function validateTasks(input) {
868
+ if (!Array.isArray(input)) throw new Error("tasks must be an array");
869
+ return input.map((t, i) => {
870
+ if (!t || typeof t !== "object") throw new Error(`tasks[${i}] must be an object`);
871
+ const rec = t;
872
+ const id = typeof rec.id === "string" && rec.id.length > 0 ? rec.id : String(i + 1);
873
+ const title = typeof rec.title === "string" ? rec.title.trim() : "";
874
+ if (!title) throw new Error(`tasks[${i}].title is required`);
875
+ const status = isValidStatus(rec.status) ? rec.status : "pending";
876
+ return { id, title, status };
877
+ });
878
+ }
879
+ var init_tasks_state = __esm({
880
+ "src/tasks-state.ts"() {
881
+ "use strict";
882
+ }
883
+ });
884
+
885
+ // src/tools/tasks.ts
886
+ var tasksSetTool;
887
+ var init_tasks = __esm({
888
+ "src/tools/tasks.ts"() {
889
+ "use strict";
890
+ init_tasks_state();
891
+ tasksSetTool = {
892
+ name: "tasks_set",
893
+ description: [
894
+ "Set the visible task list shown to the user during this turn.",
895
+ "Call this when the user has given you a multi-step job, or whenever progress changes:",
896
+ "at the start (all tasks pending, with exactly one in_progress), and after each step completes",
897
+ "(flip that task to completed and the next to in_progress).",
898
+ "Pass the ENTIRE task list each call \u2014 this replaces the panel.",
899
+ "Keep tasks short (one imperative clause). Only one should be in_progress at a time.",
900
+ "For quick single-step requests, don't use this tool at all."
901
+ ].join(" "),
902
+ parameters: {
903
+ type: "object",
904
+ properties: {
905
+ tasks: {
906
+ type: "array",
907
+ description: "The full, ordered list of tasks. Pass every call.",
908
+ items: {
909
+ type: "object",
910
+ properties: {
911
+ id: { type: "string", description: "Stable short id (e.g. '1', '2')." },
912
+ title: { type: "string", description: "Short imperative task title." },
913
+ status: {
914
+ type: "string",
915
+ enum: ["pending", "in_progress", "completed"],
916
+ description: "Only one task should be 'in_progress' at a time."
917
+ }
918
+ },
919
+ required: ["id", "title", "status"]
920
+ }
921
+ }
922
+ },
923
+ required: ["tasks"]
924
+ },
925
+ needsPermission: false,
926
+ render: (args) => ({
927
+ title: `tasks (${args.tasks.length} items)`,
928
+ body: args.tasks.map((t) => `${t.status === "completed" ? "\u2713" : t.status === "in_progress" ? "\u25B8" : "\xB7"} ${t.title}`).join("\n")
929
+ }),
930
+ run: async (args, ctx) => {
931
+ let tasks;
932
+ try {
933
+ tasks = validateTasks(args.tasks);
934
+ } catch (e) {
935
+ return `Error: ${e.message}`;
936
+ }
937
+ ctx.onTasks?.(tasks);
938
+ const summary = `${tasks.length} tasks set \u2014 ${tasks.filter((t) => t.status === "completed").length} done, ${tasks.filter((t) => t.status === "in_progress").length} active, ${tasks.filter((t) => t.status === "pending").length} pending`;
939
+ return summary;
940
+ }
941
+ };
942
+ }
943
+ });
944
+
815
945
  // src/tools/executor.ts
816
946
  function truncateForError(s) {
817
947
  return s.length <= 200 ? s : `${s.slice(0, 200)}... [${s.length - 200} more chars]`;
@@ -827,6 +957,7 @@ var init_executor = __esm({
827
957
  init_glob();
828
958
  init_grep();
829
959
  init_web_fetch();
960
+ init_tasks();
830
961
  ALL_TOOLS = [
831
962
  readTool,
832
963
  writeTool,
@@ -834,7 +965,8 @@ var init_executor = __esm({
834
965
  bashTool,
835
966
  globTool,
836
967
  grepTool,
837
- webFetchTool
968
+ webFetchTool,
969
+ tasksSetTool
838
970
  ];
839
971
  ToolExecutor = class {
840
972
  sessionAllowed = /* @__PURE__ */ new Set();
@@ -845,6 +977,9 @@ var init_executor = __esm({
845
977
  list() {
846
978
  return [...this.tools.values()];
847
979
  }
980
+ clearSessionPermissions() {
981
+ this.sessionAllowed.clear();
982
+ }
848
983
  async run(call, askPermission, ctx) {
849
984
  const tool = this.tools.get(call.name);
850
985
  if (!tool) {
@@ -904,6 +1039,85 @@ var init_executor = __esm({
904
1039
  }
905
1040
  });
906
1041
 
1042
+ // src/agent/compact.ts
1043
+ function indexOfNthUserFromEnd(messages, n) {
1044
+ let seen = 0;
1045
+ for (let i = messages.length - 1; i >= 0; i--) {
1046
+ if (messages[i].role === "user") {
1047
+ seen++;
1048
+ if (seen === n) return i;
1049
+ }
1050
+ }
1051
+ return -1;
1052
+ }
1053
+ async function compactMessages(opts2) {
1054
+ const keep = opts2.keepLastTurns ?? 4;
1055
+ const messages = opts2.messages;
1056
+ const systemMsg = messages.find((m) => m.role === "system");
1057
+ if (!systemMsg) throw new Error("compact: no system message found");
1058
+ const cutoffUserIdx = indexOfNthUserFromEnd(messages, keep);
1059
+ const firstKeepIdx = cutoffUserIdx >= 0 ? cutoffUserIdx : messages.length;
1060
+ const toSummarize = messages.slice(1, firstKeepIdx);
1061
+ const toKeep = messages.slice(firstKeepIdx);
1062
+ if (toSummarize.length === 0) {
1063
+ return { summary: "", newMessages: messages, replacedCount: 0 };
1064
+ }
1065
+ const transcript = toSummarize.map((m) => {
1066
+ if (m.role === "tool") {
1067
+ const snippet = (m.content ?? "").slice(0, 500);
1068
+ return `[tool ${m.name ?? ""}] ${snippet}`;
1069
+ }
1070
+ if (m.role === "assistant") {
1071
+ const calls = m.tool_calls ? ` (tool_calls: ${m.tool_calls.map((c) => c.function.name).join(", ")})` : "";
1072
+ return `[assistant]${calls} ${m.content ?? ""}`;
1073
+ }
1074
+ return `[${m.role}] ${m.content ?? ""}`;
1075
+ }).join("\n");
1076
+ let summary = "";
1077
+ const events = runKimi({
1078
+ accountId: opts2.accountId,
1079
+ apiToken: opts2.apiToken,
1080
+ model: opts2.model,
1081
+ messages: [
1082
+ { role: "system", content: SUMMARY_SYSTEM },
1083
+ { role: "user", content: `Summarize this session so it can be replaced by your summary:
1084
+
1085
+ ${transcript}` }
1086
+ ],
1087
+ signal: opts2.signal,
1088
+ temperature: 0.1,
1089
+ reasoningEffort: "low"
1090
+ });
1091
+ for await (const ev of events) {
1092
+ if (ev.type === "text") summary += ev.delta;
1093
+ }
1094
+ const summaryMsg = {
1095
+ role: "user",
1096
+ content: `[compacted summary of earlier turns]
1097
+ ${summary.trim()}`
1098
+ };
1099
+ return {
1100
+ summary: summary.trim(),
1101
+ newMessages: [systemMsg, summaryMsg, ...toKeep],
1102
+ replacedCount: toSummarize.length
1103
+ };
1104
+ }
1105
+ var SUMMARY_SYSTEM;
1106
+ var init_compact = __esm({
1107
+ "src/agent/compact.ts"() {
1108
+ "use strict";
1109
+ init_client();
1110
+ SUMMARY_SYSTEM = `You are summarizing a terminal coding session so it can fit back into a short context window. Produce a dense summary that captures:
1111
+ - The user's goal(s) and what they've asked for.
1112
+ - Files read or modified, with paths.
1113
+ - Tools run (bash commands, edits) and the outcome of each.
1114
+ - Decisions made and open questions.
1115
+ - Any constraints or preferences the user has stated.
1116
+
1117
+ Do not include speculation. Do not include chat-style pleasantries. Use short bullet form. Aim for ~400-800 tokens.`;
1118
+ }
1119
+ });
1120
+
907
1121
  // src/ui/diff-view.tsx
908
1122
  import { Box, Text } from "ink";
909
1123
  import { createTwoFilesPatch } from "diff";
@@ -943,9 +1157,12 @@ var init_diff_view = __esm({
943
1157
  import { Box as Box2, Text as Text2 } from "ink";
944
1158
  import Spinner from "ink-spinner";
945
1159
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
946
- function ToolView({ evt }) {
1160
+ function ToolView({ evt, verbose }) {
947
1161
  const statusIcon = evt.status === "running" ? /* @__PURE__ */ jsx2(Spinner, { type: "dots" }) : evt.status === "error" ? /* @__PURE__ */ jsx2(Text2, { color: "red", children: "\u2717" }) : /* @__PURE__ */ jsx2(Text2, { color: "green", children: "\u2713" });
948
1162
  const title = evt.render?.title ?? `${evt.name}(${compactArgs(evt.args)})`;
1163
+ const expand = Boolean(evt.expanded || verbose);
1164
+ const lines = evt.result ? evt.result.split("\n") : [];
1165
+ const showLimit = verbose ? 200 : 20;
949
1166
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginLeft: 2, children: [
950
1167
  /* @__PURE__ */ jsxs2(Text2, { children: [
951
1168
  statusIcon,
@@ -953,15 +1170,15 @@ function ToolView({ evt }) {
953
1170
  /* @__PURE__ */ jsx2(Text2, { color: "magenta", children: title })
954
1171
  ] }),
955
1172
  evt.render?.diff ? /* @__PURE__ */ jsx2(Box2, { marginLeft: 2, children: /* @__PURE__ */ jsx2(DiffView, { ...evt.render.diff }) }) : null,
956
- evt.result && evt.expanded ? /* @__PURE__ */ jsxs2(Box2, { marginLeft: 2, flexDirection: "column", children: [
957
- evt.result.split("\n").slice(0, 20).map((l, i) => /* @__PURE__ */ jsx2(Text2, { color: "gray", children: l }, i)),
958
- evt.result.split("\n").length > 20 && /* @__PURE__ */ jsxs2(Text2, { color: "gray", children: [
1173
+ evt.result && expand ? /* @__PURE__ */ jsxs2(Box2, { marginLeft: 2, flexDirection: "column", children: [
1174
+ lines.slice(0, showLimit).map((l, i) => /* @__PURE__ */ jsx2(Text2, { color: "gray", children: l }, i)),
1175
+ lines.length > showLimit && /* @__PURE__ */ jsxs2(Text2, { color: "gray", children: [
959
1176
  "... (",
960
- evt.result.split("\n").length - 20,
1177
+ lines.length - showLimit,
961
1178
  " more lines)"
962
1179
  ] })
963
1180
  ] }) : null,
964
- evt.result && !evt.expanded && evt.status !== "running" ? /* @__PURE__ */ jsxs2(Text2, { color: "gray", children: [
1181
+ evt.result && !expand && evt.status !== "running" ? /* @__PURE__ */ jsxs2(Text2, { color: "gray", children: [
965
1182
  " ",
966
1183
  firstLine(evt.result)
967
1184
  ] }) : null
@@ -985,32 +1202,32 @@ var init_tool_view = __esm({
985
1202
  // src/ui/chat.tsx
986
1203
  import { Box as Box3, Text as Text3 } from "ink";
987
1204
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
988
- function ChatView({ events, showReasoning }) {
989
- return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: events.map((e) => /* @__PURE__ */ jsx3(EventView, { evt: e, showReasoning }, e.key)) });
1205
+ function ChatView({ events, showReasoning, theme, verbose }) {
1206
+ return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: events.map((e) => /* @__PURE__ */ jsx3(EventView, { evt: e, showReasoning, theme, verbose }, e.key)) });
990
1207
  }
991
- function EventView({ evt, showReasoning }) {
1208
+ function EventView({ evt, showReasoning, theme, verbose }) {
992
1209
  if (evt.kind === "user") {
993
1210
  return /* @__PURE__ */ jsxs3(Box3, { marginY: 0, children: [
994
- /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "\u203A " }),
1211
+ /* @__PURE__ */ jsx3(Text3, { color: theme.user, children: "\u203A " }),
995
1212
  /* @__PURE__ */ jsx3(Text3, { children: evt.text })
996
1213
  ] });
997
1214
  }
998
1215
  if (evt.kind === "assistant") {
999
1216
  return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginY: 0, children: [
1000
- showReasoning && evt.reasoning ? /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", marginLeft: 2, children: /* @__PURE__ */ jsxs3(Text3, { color: "gray", dimColor: true, children: [
1217
+ showReasoning && evt.reasoning ? /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", marginLeft: 2, children: /* @__PURE__ */ jsxs3(Text3, { color: theme.reasoning.color, dimColor: theme.reasoning.dim, children: [
1001
1218
  "\u2727 thinking: ",
1002
1219
  evt.reasoning.length > 400 ? evt.reasoning.slice(0, 400) + "\u2026" : evt.reasoning
1003
1220
  ] }) }) : null,
1004
- evt.text ? /* @__PURE__ */ jsx3(Text3, { children: evt.text }) : null
1221
+ evt.text ? theme.assistant ? /* @__PURE__ */ jsx3(Text3, { color: theme.assistant, children: evt.text }) : /* @__PURE__ */ jsx3(Text3, { children: evt.text }) : null
1005
1222
  ] });
1006
1223
  }
1007
1224
  if (evt.kind === "tool") {
1008
- return /* @__PURE__ */ jsx3(ToolView, { evt });
1225
+ return /* @__PURE__ */ jsx3(ToolView, { evt, verbose });
1009
1226
  }
1010
1227
  if (evt.kind === "info") {
1011
- return /* @__PURE__ */ jsx3(Text3, { color: "gray", dimColor: true, children: evt.text });
1228
+ return /* @__PURE__ */ jsx3(Text3, { color: theme.info.color, dimColor: theme.info.dim, children: evt.text });
1012
1229
  }
1013
- return /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
1230
+ return /* @__PURE__ */ jsxs3(Text3, { color: theme.error, children: [
1014
1231
  "! ",
1015
1232
  evt.text
1016
1233
  ] });
@@ -1024,22 +1241,38 @@ var init_chat = __esm({
1024
1241
 
1025
1242
  // src/ui/status.tsx
1026
1243
  import { Box as Box4, Text as Text4 } from "ink";
1027
- import { jsx as jsx4 } from "react/jsx-runtime";
1028
- function StatusBar({ model, usage, thinking, hint }) {
1029
- const parts = [`model: ${shortModel(model)}`];
1244
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1245
+ function StatusBar({ model, usage, thinking, hint, theme, mode, effort, contextLimit }) {
1246
+ const parts = [`model: ${shortModel(model)}`, `effort: ${effort}`];
1030
1247
  if (usage) {
1031
1248
  const cached = usage.prompt_tokens_details?.cached_tokens ?? 0;
1032
1249
  const uncachedIn = usage.prompt_tokens - cached;
1033
1250
  const cost = uncachedIn * PRICE_IN_PER_M / 1e6 + cached * PRICE_IN_CACHED_PER_M / 1e6 + usage.completion_tokens * PRICE_OUT_PER_M / 1e6;
1251
+ const pct = Math.round(usage.prompt_tokens / contextLimit * 100);
1034
1252
  parts.push(
1035
1253
  `in: ${usage.prompt_tokens}${cached ? ` (${cached} cached)` : ""}`,
1036
1254
  `out: ${usage.completion_tokens}`,
1255
+ `ctx: ${pct}%`,
1037
1256
  `$${cost.toFixed(5)}`
1038
1257
  );
1039
1258
  }
1040
1259
  if (thinking) parts.push("thinking\u2026");
1041
1260
  if (hint) parts.push(hint);
1042
- return /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: parts.join(" \xB7 ") }) });
1261
+ const modeColor = mode === "plan" ? theme.modeBadge.plan : mode === "auto" ? theme.modeBadge.auto : theme.modeBadge.edit;
1262
+ const warn = usage && usage.prompt_tokens / contextLimit >= 0.8;
1263
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
1264
+ /* @__PURE__ */ jsxs4(Text4, { color: modeColor, bold: true, children: [
1265
+ "[",
1266
+ mode,
1267
+ "]"
1268
+ ] }),
1269
+ /* @__PURE__ */ jsx4(Text4, { children: " " }),
1270
+ /* @__PURE__ */ jsx4(Text4, { color: theme.info.color, dimColor: theme.info.dim, children: parts.join(" \xB7 ") }),
1271
+ warn ? /* @__PURE__ */ jsxs4(Text4, { color: theme.warn, bold: true, children: [
1272
+ " \xB7 ",
1273
+ "/compact recommended"
1274
+ ] }) : null
1275
+ ] });
1043
1276
  }
1044
1277
  function shortModel(m) {
1045
1278
  const last = m.split("/").at(-1) ?? m;
@@ -1058,25 +1291,25 @@ var init_status = __esm({
1058
1291
  // src/ui/permission.tsx
1059
1292
  import { Box as Box5, Text as Text5 } from "ink";
1060
1293
  import SelectInput from "ink-select-input";
1061
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1062
- function PermissionModal({ tool, args, onDecide }) {
1294
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1295
+ function PermissionModal({ tool, args, onDecide, theme }) {
1063
1296
  const render2 = tool.render?.(args);
1064
1297
  const items = [
1065
1298
  { label: "Allow once", value: "allow" },
1066
1299
  { label: "Allow for this session", value: "allow_session" },
1067
1300
  { label: "Deny", value: "deny" }
1068
1301
  ];
1069
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
1070
- /* @__PURE__ */ jsx5(Text5, { color: "yellow", bold: true, children: "Permission requested" }),
1071
- /* @__PURE__ */ jsxs4(Text5, { children: [
1302
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", borderStyle: "round", borderColor: theme.permission, paddingX: 1, children: [
1303
+ /* @__PURE__ */ jsx5(Text5, { color: theme.permission, bold: true, children: "Permission requested" }),
1304
+ /* @__PURE__ */ jsxs5(Text5, { children: [
1072
1305
  "tool: ",
1073
- /* @__PURE__ */ jsx5(Text5, { color: "magenta", children: tool.name })
1306
+ /* @__PURE__ */ jsx5(Text5, { color: theme.tool, children: tool.name })
1074
1307
  ] }),
1075
- render2?.title ? /* @__PURE__ */ jsxs4(Text5, { children: [
1308
+ render2?.title ? /* @__PURE__ */ jsxs5(Text5, { children: [
1076
1309
  "action: ",
1077
1310
  render2.title
1078
1311
  ] }) : null,
1079
- render2?.diff ? /* @__PURE__ */ jsx5(Box5, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx5(DiffView, { ...render2.diff }) }) : /* @__PURE__ */ jsxs4(Text5, { color: "gray", children: [
1312
+ render2?.diff ? /* @__PURE__ */ jsx5(Box5, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx5(DiffView, { ...render2.diff }) }) : /* @__PURE__ */ jsxs5(Text5, { color: theme.info.color, dimColor: theme.info.dim, children: [
1080
1313
  "args: ",
1081
1314
  JSON.stringify(args)
1082
1315
  ] }),
@@ -1090,6 +1323,150 @@ var init_permission = __esm({
1090
1323
  }
1091
1324
  });
1092
1325
 
1326
+ // src/ui/resume-picker.tsx
1327
+ import { Box as Box6, Text as Text6 } from "ink";
1328
+ import SelectInput2 from "ink-select-input";
1329
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1330
+ function ResumePicker({ sessions, onPick, theme }) {
1331
+ if (sessions.length === 0) {
1332
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, children: [
1333
+ /* @__PURE__ */ jsx6(Text6, { color: theme.accent, bold: true, children: "Resume a session" }),
1334
+ /* @__PURE__ */ jsx6(Text6, { color: theme.info.color, dimColor: theme.info.dim, children: "No saved sessions yet. Press Enter to dismiss." }),
1335
+ /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(
1336
+ SelectInput2,
1337
+ {
1338
+ items: [{ label: "(back)", value: "__cancel__" }],
1339
+ onSelect: () => onPick(null)
1340
+ }
1341
+ ) })
1342
+ ] });
1343
+ }
1344
+ const items = sessions.map((s) => ({
1345
+ label: `${formatDate(s.updatedAt)} \xB7 ${s.messageCount} msgs \xB7 ${s.firstPrompt}`,
1346
+ value: s.id
1347
+ }));
1348
+ items.push({ label: "(cancel)", value: "__cancel__" });
1349
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, children: [
1350
+ /* @__PURE__ */ jsx6(Text6, { color: theme.accent, bold: true, children: "Resume a session" }),
1351
+ /* @__PURE__ */ jsx6(Text6, { color: theme.info.color, dimColor: theme.info.dim, children: "Arrow keys to select, Enter to confirm." }),
1352
+ /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(
1353
+ SelectInput2,
1354
+ {
1355
+ items,
1356
+ onSelect: (item) => {
1357
+ if (item.value === "__cancel__") return onPick(null);
1358
+ const picked = sessions.find((s) => s.id === item.value) ?? null;
1359
+ onPick(picked);
1360
+ }
1361
+ }
1362
+ ) })
1363
+ ] });
1364
+ }
1365
+ function formatDate(iso) {
1366
+ try {
1367
+ const d = new Date(iso);
1368
+ return d.toLocaleString(void 0, {
1369
+ month: "short",
1370
+ day: "numeric",
1371
+ hour: "2-digit",
1372
+ minute: "2-digit"
1373
+ });
1374
+ } catch {
1375
+ return iso;
1376
+ }
1377
+ }
1378
+ var init_resume_picker = __esm({
1379
+ "src/ui/resume-picker.tsx"() {
1380
+ "use strict";
1381
+ }
1382
+ });
1383
+
1384
+ // src/ui/task-list.tsx
1385
+ import { useEffect, useState } from "react";
1386
+ import { Box as Box7, Text as Text7 } from "ink";
1387
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1388
+ function TaskList({ tasks, theme, startedAt, tokensDelta }) {
1389
+ const [now, setNow] = useState(Date.now());
1390
+ useEffect(() => {
1391
+ if (startedAt === null) return;
1392
+ const allDone2 = tasks.length > 0 && tasks.every((t) => t.status === "completed");
1393
+ if (allDone2) return;
1394
+ const id = setInterval(() => setNow(Date.now()), 1e3);
1395
+ return () => clearInterval(id);
1396
+ }, [startedAt, tasks]);
1397
+ if (tasks.length === 0) return null;
1398
+ const active = tasks.find((t) => t.status === "in_progress");
1399
+ const done = tasks.filter((t) => t.status === "completed").length;
1400
+ const total = tasks.length;
1401
+ const allDone = done === total;
1402
+ const header = active ? active.title : allDone ? `Done (${total} tasks)` : `Tasks (${done}/${total})`;
1403
+ const elapsed = startedAt ? formatElapsed(now - startedAt) : null;
1404
+ const headerStats = [elapsed, tokensDelta > 0 ? `\u2191 ${formatTokens(tokensDelta)} tokens` : null].filter(Boolean).join(" \xB7 ");
1405
+ const visibleTasks = tasks.slice(0, MAX_VISIBLE);
1406
+ const hiddenPending = Math.max(0, tasks.length - visibleTasks.length);
1407
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginBottom: 1, children: [
1408
+ /* @__PURE__ */ jsxs7(Box7, { children: [
1409
+ /* @__PURE__ */ jsxs7(Text7, { color: allDone ? "green" : theme.accent, bold: true, children: [
1410
+ allDone ? "\u2713" : "\u25B8",
1411
+ " ",
1412
+ header
1413
+ ] }),
1414
+ headerStats && /* @__PURE__ */ jsxs7(Text7, { color: theme.info.color, dimColor: theme.info.dim, children: [
1415
+ " ",
1416
+ "(",
1417
+ headerStats,
1418
+ ")"
1419
+ ] })
1420
+ ] }),
1421
+ visibleTasks.map((t) => /* @__PURE__ */ jsx7(TaskRow, { task: t, theme }, t.id)),
1422
+ hiddenPending > 0 && /* @__PURE__ */ jsxs7(Text7, { color: theme.info.color, dimColor: theme.info.dim, children: [
1423
+ " ",
1424
+ "\u2026 +",
1425
+ hiddenPending,
1426
+ " more"
1427
+ ] })
1428
+ ] });
1429
+ }
1430
+ function TaskRow({ task, theme }) {
1431
+ if (task.status === "completed") {
1432
+ return /* @__PURE__ */ jsxs7(Text7, { color: theme.info.color, dimColor: theme.info.dim, children: [
1433
+ " ",
1434
+ "\u2713 ",
1435
+ /* @__PURE__ */ jsx7(Text7, { strikethrough: true, children: task.title })
1436
+ ] });
1437
+ }
1438
+ if (task.status === "in_progress") {
1439
+ return /* @__PURE__ */ jsxs7(Text7, { color: theme.accent, bold: true, children: [
1440
+ " ",
1441
+ "\u25A0 ",
1442
+ task.title
1443
+ ] });
1444
+ }
1445
+ return /* @__PURE__ */ jsxs7(Text7, { color: theme.info.color, dimColor: theme.info.dim, children: [
1446
+ " ",
1447
+ "\u2610 ",
1448
+ task.title
1449
+ ] });
1450
+ }
1451
+ function formatElapsed(ms) {
1452
+ const total = Math.floor(ms / 1e3);
1453
+ const m = Math.floor(total / 60);
1454
+ const s = total % 60;
1455
+ if (m === 0) return `${s}s`;
1456
+ return `${m}m ${s}s`;
1457
+ }
1458
+ function formatTokens(n) {
1459
+ if (n < 1e3) return String(n);
1460
+ return `${(n / 1e3).toFixed(1)}k`;
1461
+ }
1462
+ var MAX_VISIBLE;
1463
+ var init_task_list = __esm({
1464
+ "src/ui/task-list.tsx"() {
1465
+ "use strict";
1466
+ MAX_VISIBLE = 6;
1467
+ }
1468
+ });
1469
+
1093
1470
  // node_modules/chalk/source/vendor/ansi-styles/index.js
1094
1471
  function assembleStyles() {
1095
1472
  const codes = /* @__PURE__ */ new Map();
@@ -1611,9 +1988,20 @@ var init_source = __esm({
1611
1988
  });
1612
1989
 
1613
1990
  // src/ui/text-input.tsx
1614
- import { useState, useEffect } from "react";
1615
- import { Text as Text6, useInput } from "ink";
1616
- import { jsx as jsx6 } from "react/jsx-runtime";
1991
+ import { useState as useState2, useEffect as useEffect2, useRef } from "react";
1992
+ import { Text as Text8, useInput } from "ink";
1993
+ import { jsx as jsx8 } from "react/jsx-runtime";
1994
+ function shouldTreatAsPaste(input) {
1995
+ if (input.length >= PASTE_CHAR_THRESHOLD) return true;
1996
+ const newlines = (input.match(/\n/g) ?? []).length;
1997
+ return newlines >= PASTE_NEWLINE_THRESHOLD;
1998
+ }
1999
+ function newPasteId() {
2000
+ return Math.random().toString(36).slice(2, 7);
2001
+ }
2002
+ function countLines(s) {
2003
+ return s.split("\n").length;
2004
+ }
1617
2005
  function findWordBoundaryForward(text, pos) {
1618
2006
  while (pos < text.length && /\w/.test(text[pos])) pos++;
1619
2007
  while (pos < text.length && !/\w/.test(text[pos])) pos++;
@@ -1632,10 +2020,12 @@ function CustomTextInput({
1632
2020
  onHistoryDown,
1633
2021
  onClearQueueItem,
1634
2022
  focus = true,
1635
- mask
2023
+ mask,
2024
+ enablePaste = false
1636
2025
  }) {
1637
- const [cursorOffset, setCursorOffset] = useState(value.length);
1638
- useEffect(() => {
2026
+ const [cursorOffset, setCursorOffset] = useState2(value.length);
2027
+ const pastesRef = useRef(/* @__PURE__ */ new Map());
2028
+ useEffect2(() => {
1639
2029
  if (!focus) return;
1640
2030
  setCursorOffset((prev) => prev > value.length ? value.length : prev);
1641
2031
  }, [value, focus]);
@@ -1644,9 +2034,21 @@ function CustomTextInput({
1644
2034
  if (!focus) return;
1645
2035
  if (key.ctrl && input === "c") return;
1646
2036
  if (key.ctrl && input === "r") return;
2037
+ if (key.ctrl && input === "o") return;
1647
2038
  if (key.tab) return;
1648
2039
  if (key.return) {
1649
- onSubmit(value);
2040
+ let full = value;
2041
+ let hasPastes = false;
2042
+ if (enablePaste && pastesRef.current.size > 0) {
2043
+ for (const [placeholder, fullText] of pastesRef.current) {
2044
+ if (full.includes(placeholder)) {
2045
+ full = full.split(placeholder).join(fullText);
2046
+ hasPastes = true;
2047
+ }
2048
+ }
2049
+ }
2050
+ onSubmit(full, hasPastes ? value : void 0);
2051
+ pastesRef.current.clear();
1650
2052
  setCursorOffset(0);
1651
2053
  return;
1652
2054
  }
@@ -1673,13 +2075,27 @@ function CustomTextInput({
1673
2075
  } else {
1674
2076
  nextCursor = cursorOffset + 1;
1675
2077
  }
2078
+ } else if (key.meta && input === "b") {
2079
+ nextCursor = findWordBoundaryBackward(value, cursorOffset);
2080
+ } else if (key.meta && input === "f") {
2081
+ nextCursor = findWordBoundaryForward(value, cursorOffset);
2082
+ } else if (key.meta && input === "d") {
2083
+ didDelete = true;
2084
+ const boundary = findWordBoundaryForward(value, cursorOffset);
2085
+ nextValue = value.slice(0, cursorOffset) + value.slice(boundary);
1676
2086
  } else if (key.home || key.ctrl && input === "a") {
1677
2087
  nextCursor = 0;
1678
2088
  } else if (key.end || key.ctrl && input === "e") {
1679
2089
  nextCursor = value.length;
1680
2090
  } else if (key.backspace) {
1681
2091
  didDelete = true;
1682
- if (key.meta || key.ctrl && input === "w") {
2092
+ const tokenBoundary = enablePaste ? findPasteTokenEndingAt(value, cursorOffset, pastesRef.current) : -1;
2093
+ if (tokenBoundary >= 0) {
2094
+ const token = value.slice(tokenBoundary, cursorOffset);
2095
+ pastesRef.current.delete(token);
2096
+ nextValue = value.slice(0, tokenBoundary) + value.slice(cursorOffset);
2097
+ nextCursor = tokenBoundary;
2098
+ } else if (key.meta || key.ctrl && input === "w") {
1683
2099
  const boundary = findWordBoundaryBackward(value, cursorOffset);
1684
2100
  nextValue = value.slice(0, boundary) + value.slice(cursorOffset);
1685
2101
  nextCursor = boundary;
@@ -1714,8 +2130,16 @@ function CustomTextInput({
1714
2130
  didDelete = true;
1715
2131
  nextValue = value.slice(0, cursorOffset);
1716
2132
  } else if (input.length > 0 && !key.ctrl && !key.meta) {
1717
- nextValue = value.slice(0, cursorOffset) + input + value.slice(cursorOffset);
1718
- nextCursor = cursorOffset + input.length;
2133
+ let toInsert = input;
2134
+ if (enablePaste && shouldTreatAsPaste(input)) {
2135
+ const lines = countLines(input);
2136
+ const id = newPasteId();
2137
+ const placeholder = `[pasted ${lines} line${lines === 1 ? "" : "s"} #${id}]`;
2138
+ pastesRef.current.set(placeholder, input);
2139
+ toInsert = placeholder;
2140
+ }
2141
+ nextValue = value.slice(0, cursorOffset) + toInsert + value.slice(cursorOffset);
2142
+ nextCursor = cursorOffset + toInsert.length;
1719
2143
  }
1720
2144
  if (nextCursor < 0) nextCursor = 0;
1721
2145
  if (nextCursor > nextValue.length) nextCursor = nextValue.length;
@@ -1743,12 +2167,24 @@ function CustomTextInput({
1743
2167
  } else if (cursorOffset === displayValue.length) {
1744
2168
  renderedValue += source_default.inverse(" ");
1745
2169
  }
1746
- return /* @__PURE__ */ jsx6(Text6, { children: renderedValue });
2170
+ return /* @__PURE__ */ jsx8(Text8, { children: renderedValue });
2171
+ }
2172
+ function findPasteTokenEndingAt(value, pos, pastes) {
2173
+ if (pos <= 0 || value[pos - 1] !== "]") return -1;
2174
+ for (const placeholder of pastes.keys()) {
2175
+ if (placeholder.length > pos) continue;
2176
+ const start = pos - placeholder.length;
2177
+ if (value.slice(start, pos) === placeholder) return start;
2178
+ }
2179
+ return -1;
1747
2180
  }
2181
+ var PASTE_CHAR_THRESHOLD, PASTE_NEWLINE_THRESHOLD;
1748
2182
  var init_text_input = __esm({
1749
2183
  "src/ui/text-input.tsx"() {
1750
2184
  "use strict";
1751
2185
  init_source();
2186
+ PASTE_CHAR_THRESHOLD = 200;
2187
+ PASTE_NEWLINE_THRESHOLD = 3;
1752
2188
  }
1753
2189
  });
1754
2190
 
@@ -1792,12 +2228,12 @@ async function writeCache(entry) {
1792
2228
  }
1793
2229
  async function fetchLatestVersion() {
1794
2230
  try {
1795
- const res = await fetch(GITHUB_API, {
1796
- headers: { "User-Agent": "kimiflare-update-checker" }
2231
+ const res = await fetch(NPM_REGISTRY, {
2232
+ headers: { "User-Agent": "kimiflare-update-checker", Accept: "application/json" }
1797
2233
  });
1798
2234
  if (!res.ok) return null;
1799
2235
  const data = await res.json();
1800
- return data.tag_name ?? null;
2236
+ return data.version ?? null;
1801
2237
  } catch {
1802
2238
  return null;
1803
2239
  }
@@ -1840,25 +2276,25 @@ async function isGitRepo() {
1840
2276
  return false;
1841
2277
  }
1842
2278
  }
1843
- var CACHE_TTL_MS, GITHUB_API;
2279
+ var CACHE_TTL_MS, NPM_REGISTRY;
1844
2280
  var init_update_check = __esm({
1845
2281
  "src/util/update-check.ts"() {
1846
2282
  "use strict";
1847
2283
  CACHE_TTL_MS = 60 * 60 * 1e3;
1848
- GITHUB_API = "https://api.github.com/repos/sinameraji/kimiflare/releases/latest";
2284
+ NPM_REGISTRY = "https://registry.npmjs.org/kimiflare/latest";
1849
2285
  }
1850
2286
  });
1851
2287
 
1852
2288
  // src/ui/onboarding.tsx
1853
- import { useState as useState2 } from "react";
1854
- import { Box as Box6, Text as Text7 } from "ink";
1855
- import { Fragment, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
2289
+ import { useState as useState3 } from "react";
2290
+ import { Box as Box8, Text as Text9 } from "ink";
2291
+ import { Fragment, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1856
2292
  function Onboarding({ onDone }) {
1857
- const [step, setStep] = useState2("accountId");
1858
- const [accountId, setAccountId] = useState2("");
1859
- const [apiToken, setApiToken] = useState2("");
1860
- const [model, setModel] = useState2(DEFAULT_MODEL);
1861
- const [savedPath, setSavedPath] = useState2(null);
2293
+ const [step, setStep] = useState3("accountId");
2294
+ const [accountId, setAccountId] = useState3("");
2295
+ const [apiToken, setApiToken] = useState3("");
2296
+ const [model, setModel] = useState3(DEFAULT_MODEL);
2297
+ const [savedPath, setSavedPath] = useState3(null);
1862
2298
  const handleAccountIdSubmit = (value) => {
1863
2299
  const trimmed = value.trim();
1864
2300
  if (!trimmed) return;
@@ -1886,15 +2322,15 @@ function Onboarding({ onDone }) {
1886
2322
  setSavedPath(`error: ${e.message}`);
1887
2323
  }
1888
2324
  };
1889
- return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
1890
- /* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: "Welcome to kimiflare!" }),
1891
- /* @__PURE__ */ jsx7(Text7, { color: "gray", dimColor: true, children: "Terminal coding agent powered by Kimi-K2.6 on Cloudflare Workers AI." }),
1892
- /* @__PURE__ */ jsxs5(Box6, { marginTop: 1, flexDirection: "column", children: [
1893
- step === "accountId" && /* @__PURE__ */ jsxs5(Fragment, { children: [
1894
- /* @__PURE__ */ jsx7(Text7, { children: "Enter your Cloudflare Account ID:" }),
1895
- /* @__PURE__ */ jsxs5(Box6, { children: [
1896
- /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "\u203A " }),
1897
- /* @__PURE__ */ jsx7(
2325
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2326
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: "cyan", children: "Welcome to kimiflare!" }),
2327
+ /* @__PURE__ */ jsx9(Text9, { color: "gray", dimColor: true, children: "Terminal coding agent powered by Kimi-K2.6 on Cloudflare Workers AI." }),
2328
+ /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexDirection: "column", children: [
2329
+ step === "accountId" && /* @__PURE__ */ jsxs8(Fragment, { children: [
2330
+ /* @__PURE__ */ jsx9(Text9, { children: "Enter your Cloudflare Account ID:" }),
2331
+ /* @__PURE__ */ jsxs8(Box8, { children: [
2332
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: "\u203A " }),
2333
+ /* @__PURE__ */ jsx9(
1898
2334
  CustomTextInput,
1899
2335
  {
1900
2336
  value: accountId,
@@ -1904,12 +2340,12 @@ function Onboarding({ onDone }) {
1904
2340
  )
1905
2341
  ] })
1906
2342
  ] }),
1907
- step === "apiToken" && /* @__PURE__ */ jsxs5(Fragment, { children: [
1908
- /* @__PURE__ */ jsx7(Text7, { children: "Enter your Cloudflare API Token:" }),
1909
- /* @__PURE__ */ jsx7(Text7, { color: "gray", dimColor: true, children: "Create one at https://dash.cloudflare.com/profile/api-tokens" }),
1910
- /* @__PURE__ */ jsxs5(Box6, { children: [
1911
- /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "\u203A " }),
1912
- /* @__PURE__ */ jsx7(
2343
+ step === "apiToken" && /* @__PURE__ */ jsxs8(Fragment, { children: [
2344
+ /* @__PURE__ */ jsx9(Text9, { children: "Enter your Cloudflare API Token:" }),
2345
+ /* @__PURE__ */ jsx9(Text9, { color: "gray", dimColor: true, children: "Create one at https://dash.cloudflare.com/profile/api-tokens" }),
2346
+ /* @__PURE__ */ jsxs8(Box8, { children: [
2347
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: "\u203A " }),
2348
+ /* @__PURE__ */ jsx9(
1913
2349
  CustomTextInput,
1914
2350
  {
1915
2351
  value: apiToken,
@@ -1920,15 +2356,15 @@ function Onboarding({ onDone }) {
1920
2356
  )
1921
2357
  ] })
1922
2358
  ] }),
1923
- step === "model" && /* @__PURE__ */ jsxs5(Fragment, { children: [
1924
- /* @__PURE__ */ jsx7(Text7, { children: "Model ID (press Enter for default):" }),
1925
- /* @__PURE__ */ jsxs5(Text7, { color: "gray", dimColor: true, children: [
2359
+ step === "model" && /* @__PURE__ */ jsxs8(Fragment, { children: [
2360
+ /* @__PURE__ */ jsx9(Text9, { children: "Model ID (press Enter for default):" }),
2361
+ /* @__PURE__ */ jsxs8(Text9, { color: "gray", dimColor: true, children: [
1926
2362
  "default: ",
1927
2363
  DEFAULT_MODEL
1928
2364
  ] }),
1929
- /* @__PURE__ */ jsxs5(Box6, { children: [
1930
- /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "\u203A " }),
1931
- /* @__PURE__ */ jsx7(
2365
+ /* @__PURE__ */ jsxs8(Box8, { children: [
2366
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: "\u203A " }),
2367
+ /* @__PURE__ */ jsx9(
1932
2368
  CustomTextInput,
1933
2369
  {
1934
2370
  value: model,
@@ -1938,26 +2374,26 @@ function Onboarding({ onDone }) {
1938
2374
  )
1939
2375
  ] })
1940
2376
  ] }),
1941
- step === "confirm" && /* @__PURE__ */ jsxs5(Fragment, { children: [
1942
- /* @__PURE__ */ jsx7(Text7, { children: "Ready to save configuration:" }),
1943
- /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", marginLeft: 2, children: [
1944
- /* @__PURE__ */ jsxs5(Text7, { color: "gray", children: [
2377
+ step === "confirm" && /* @__PURE__ */ jsxs8(Fragment, { children: [
2378
+ /* @__PURE__ */ jsx9(Text9, { children: "Ready to save configuration:" }),
2379
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginLeft: 2, children: [
2380
+ /* @__PURE__ */ jsxs8(Text9, { color: "gray", children: [
1945
2381
  "Account ID: ",
1946
2382
  accountId
1947
2383
  ] }),
1948
- /* @__PURE__ */ jsxs5(Text7, { color: "gray", children: [
2384
+ /* @__PURE__ */ jsxs8(Text9, { color: "gray", children: [
1949
2385
  "API Token: ",
1950
2386
  "\u2022".repeat(apiToken.length)
1951
2387
  ] }),
1952
- /* @__PURE__ */ jsxs5(Text7, { color: "gray", children: [
2388
+ /* @__PURE__ */ jsxs8(Text9, { color: "gray", children: [
1953
2389
  "Model: ",
1954
2390
  model
1955
2391
  ] })
1956
2392
  ] }),
1957
- /* @__PURE__ */ jsx7(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { children: "Press Enter to confirm, or Ctrl+C to cancel" }) }),
1958
- /* @__PURE__ */ jsxs5(Box6, { children: [
1959
- /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "\u203A " }),
1960
- /* @__PURE__ */ jsx7(
2393
+ /* @__PURE__ */ jsx9(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { children: "Press Enter to confirm, or Ctrl+C to cancel" }) }),
2394
+ /* @__PURE__ */ jsxs8(Box8, { children: [
2395
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: "\u203A " }),
2396
+ /* @__PURE__ */ jsx9(
1961
2397
  CustomTextInput,
1962
2398
  {
1963
2399
  value: "",
@@ -1968,7 +2404,7 @@ function Onboarding({ onDone }) {
1968
2404
  )
1969
2405
  ] })
1970
2406
  ] }),
1971
- savedPath && /* @__PURE__ */ jsxs5(Text7, { color: "green", children: [
2407
+ savedPath && /* @__PURE__ */ jsxs8(Text9, { color: "green", children: [
1972
2408
  "Config saved to ",
1973
2409
  savedPath
1974
2410
  ] })
@@ -1983,51 +2419,266 @@ var init_onboarding = __esm({
1983
2419
  }
1984
2420
  });
1985
2421
 
2422
+ // src/ui/theme.ts
2423
+ function resolveTheme(name) {
2424
+ if (!name) return THEMES[DEFAULT_THEME_NAME];
2425
+ return THEMES[name] ?? THEMES[DEFAULT_THEME_NAME];
2426
+ }
2427
+ function themeNames() {
2428
+ return Object.keys(THEMES);
2429
+ }
2430
+ var dark, light, highContrast, THEMES, DEFAULT_THEME_NAME;
2431
+ var init_theme = __esm({
2432
+ "src/ui/theme.ts"() {
2433
+ "use strict";
2434
+ dark = {
2435
+ name: "dark",
2436
+ label: "dark (default \u2014 for dark terminals)",
2437
+ user: "cyan",
2438
+ assistant: void 0,
2439
+ reasoning: { color: "gray", dim: true },
2440
+ info: { color: "gray", dim: true },
2441
+ error: "red",
2442
+ warn: "yellow",
2443
+ tool: "magenta",
2444
+ spinner: "yellow",
2445
+ permission: "yellow",
2446
+ queue: { color: "gray", dim: true },
2447
+ accent: "cyan",
2448
+ modeBadge: { plan: "blue", auto: "green", edit: "cyan" }
2449
+ };
2450
+ light = {
2451
+ name: "light",
2452
+ label: "light (for bright terminal backgrounds)",
2453
+ user: "blue",
2454
+ assistant: void 0,
2455
+ reasoning: { color: "blackBright", dim: false },
2456
+ info: { color: "blackBright", dim: false },
2457
+ error: "red",
2458
+ warn: "magenta",
2459
+ tool: "magenta",
2460
+ spinner: "blue",
2461
+ permission: "magenta",
2462
+ queue: { color: "blackBright", dim: false },
2463
+ accent: "blue",
2464
+ modeBadge: { plan: "blue", auto: "green", edit: "magenta" }
2465
+ };
2466
+ highContrast = {
2467
+ name: "high-contrast",
2468
+ label: "high-contrast (bold, bright colors for low-vision)",
2469
+ user: "cyanBright",
2470
+ assistant: "whiteBright",
2471
+ reasoning: { color: "whiteBright", dim: false },
2472
+ info: { color: "whiteBright", dim: false },
2473
+ error: "redBright",
2474
+ warn: "yellowBright",
2475
+ tool: "magentaBright",
2476
+ spinner: "yellowBright",
2477
+ permission: "yellowBright",
2478
+ queue: { color: "whiteBright", dim: false },
2479
+ accent: "cyanBright",
2480
+ modeBadge: { plan: "blueBright", auto: "greenBright", edit: "cyanBright" }
2481
+ };
2482
+ THEMES = {
2483
+ dark,
2484
+ light,
2485
+ "high-contrast": highContrast
2486
+ };
2487
+ DEFAULT_THEME_NAME = "dark";
2488
+ }
2489
+ });
2490
+
2491
+ // src/sessions.ts
2492
+ import { readFile as readFile7, writeFile as writeFile5, mkdir as mkdir4, readdir, stat as stat2 } from "fs/promises";
2493
+ import { homedir as homedir5 } from "os";
2494
+ import { join as join3 } from "path";
2495
+ function sessionsDir() {
2496
+ const xdg = process.env.XDG_DATA_HOME || join3(homedir5(), ".local", "share");
2497
+ return join3(xdg, "kimiflare", "sessions");
2498
+ }
2499
+ function sanitize(text) {
2500
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
2501
+ }
2502
+ function makeSessionId(firstPrompt) {
2503
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2504
+ const slug = sanitize(firstPrompt) || "session";
2505
+ return `${ts}_${slug}`;
2506
+ }
2507
+ async function saveSession(file) {
2508
+ const dir = sessionsDir();
2509
+ await mkdir4(dir, { recursive: true });
2510
+ const path = join3(dir, `${file.id}.json`);
2511
+ await writeFile5(path, JSON.stringify(file, null, 2), "utf8");
2512
+ return path;
2513
+ }
2514
+ async function listSessions(limit = 30) {
2515
+ const dir = sessionsDir();
2516
+ let entries;
2517
+ try {
2518
+ entries = await readdir(dir);
2519
+ } catch {
2520
+ return [];
2521
+ }
2522
+ const summaries = [];
2523
+ for (const name of entries) {
2524
+ if (!name.endsWith(".json")) continue;
2525
+ const path = join3(dir, name);
2526
+ try {
2527
+ const [s, raw] = await Promise.all([stat2(path), readFile7(path, "utf8")]);
2528
+ const parsed = JSON.parse(raw);
2529
+ const firstUser = parsed.messages.find((m) => m.role === "user");
2530
+ const firstPrompt = typeof firstUser?.content === "string" ? firstUser.content : "(no prompt)";
2531
+ summaries.push({
2532
+ id: parsed.id,
2533
+ filePath: path,
2534
+ cwd: parsed.cwd,
2535
+ firstPrompt: firstPrompt.slice(0, 80),
2536
+ messageCount: parsed.messages.filter((m) => m.role !== "system").length,
2537
+ updatedAt: parsed.updatedAt ?? s.mtime.toISOString()
2538
+ });
2539
+ } catch {
2540
+ }
2541
+ }
2542
+ summaries.sort((a, b) => b.updatedAt < a.updatedAt ? -1 : 1);
2543
+ return summaries.slice(0, limit);
2544
+ }
2545
+ async function loadSession(filePath) {
2546
+ const raw = await readFile7(filePath, "utf8");
2547
+ return JSON.parse(raw);
2548
+ }
2549
+ var init_sessions = __esm({
2550
+ "src/sessions.ts"() {
2551
+ "use strict";
2552
+ }
2553
+ });
2554
+
1986
2555
  // src/app.tsx
1987
2556
  var app_exports = {};
1988
2557
  __export(app_exports, {
1989
2558
  renderApp: () => renderApp
1990
2559
  });
1991
- import { useState as useState3, useRef, useEffect as useEffect2, useCallback } from "react";
1992
- import { Box as Box7, Text as Text8, useApp, useInput as useInput2, render } from "ink";
2560
+ import { useState as useState4, useRef as useRef2, useEffect as useEffect3, useCallback } from "react";
2561
+ import { Box as Box9, Text as Text10, useApp, useInput as useInput2, render } from "ink";
1993
2562
  import Spinner2 from "ink-spinner";
1994
2563
  import { unlink } from "fs/promises";
1995
- import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
2564
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1996
2565
  function App({ initialCfg }) {
1997
2566
  const { exit } = useApp();
1998
- const [cfg, setCfg] = useState3(initialCfg);
1999
- const [events, setEvents] = useState3([
2000
- { kind: "info", key: mkKey(), text: "kimiflare \xB7 /help for commands \xB7 ctrl-c to exit" }
2567
+ const [cfg, setCfg] = useState4(initialCfg);
2568
+ const [events, setEvents] = useState4([
2569
+ { kind: "info", key: mkKey(), text: "kimiflare \xB7 /help for commands \xB7 ctrl-c to exit \xB7 shift+tab to cycle modes" }
2001
2570
  ]);
2002
- const [input, setInput] = useState3("");
2003
- const [busy, setBusy] = useState3(false);
2004
- const [usage, setUsage] = useState3(null);
2005
- const [showReasoning, setShowReasoning] = useState3(false);
2006
- const [perm, setPerm] = useState3(null);
2007
- const [queue, setQueue] = useState3([]);
2008
- const [history, setHistory] = useState3([]);
2009
- const [historyIndex, setHistoryIndex] = useState3(-1);
2010
- const [draftInput, setDraftInput] = useState3("");
2011
- const [updateInfo, setUpdateInfo] = useState3(null);
2012
- const messagesRef = useRef([
2571
+ const [input, setInput] = useState4("");
2572
+ const [busy, setBusy] = useState4(false);
2573
+ const [usage, setUsage] = useState4(null);
2574
+ const [showReasoning, setShowReasoning] = useState4(false);
2575
+ const [perm, setPerm] = useState4(null);
2576
+ const [queue, setQueue] = useState4([]);
2577
+ const [history, setHistory] = useState4([]);
2578
+ const [historyIndex, setHistoryIndex] = useState4(-1);
2579
+ const [draftInput, setDraftInput] = useState4("");
2580
+ const [updateInfo, setUpdateInfo] = useState4(null);
2581
+ const [mode, setMode] = useState4("edit");
2582
+ const [effort, setEffort] = useState4(
2583
+ initialCfg?.reasoningEffort ?? DEFAULT_REASONING_EFFORT
2584
+ );
2585
+ const [theme, setTheme] = useState4(resolveTheme(initialCfg?.theme));
2586
+ const [resumeSessions, setResumeSessions] = useState4(null);
2587
+ const [tasks, setTasks] = useState4([]);
2588
+ const [tasksStartedAt, setTasksStartedAt] = useState4(null);
2589
+ const [tasksStartTokens, setTasksStartTokens] = useState4(0);
2590
+ const [verbose, setVerbose] = useState4(false);
2591
+ const messagesRef = useRef2([
2013
2592
  {
2014
2593
  role: "system",
2015
- content: buildSystemPrompt({ cwd: process.cwd(), tools: ALL_TOOLS, model: cfg?.model ?? DEFAULT_MODEL })
2594
+ content: buildSystemPrompt({
2595
+ cwd: process.cwd(),
2596
+ tools: ALL_TOOLS,
2597
+ model: cfg?.model ?? DEFAULT_MODEL,
2598
+ mode: "edit"
2599
+ })
2016
2600
  }
2017
2601
  ]);
2018
- const executorRef = useRef(new ToolExecutor(ALL_TOOLS));
2019
- const activeAsstIdRef = useRef(null);
2020
- const activeControllerRef = useRef(null);
2602
+ const executorRef = useRef2(new ToolExecutor(ALL_TOOLS));
2603
+ const activeAsstIdRef = useRef2(null);
2604
+ const activeControllerRef = useRef2(null);
2605
+ const sessionIdRef = useRef2(null);
2606
+ const modeRef = useRef2(mode);
2607
+ const effortRef = useRef2(effort);
2608
+ useEffect3(() => {
2609
+ modeRef.current = mode;
2610
+ messagesRef.current[0] = {
2611
+ role: "system",
2612
+ content: buildSystemPrompt({
2613
+ cwd: process.cwd(),
2614
+ tools: ALL_TOOLS,
2615
+ model: cfg?.model ?? DEFAULT_MODEL,
2616
+ mode
2617
+ })
2618
+ };
2619
+ if (mode === "plan") {
2620
+ executorRef.current.clearSessionPermissions();
2621
+ }
2622
+ }, [mode, cfg?.model]);
2623
+ useEffect3(() => {
2624
+ effortRef.current = effort;
2625
+ }, [effort]);
2626
+ const saveSessionSafe = useCallback(async () => {
2627
+ if (!cfg) return;
2628
+ if (!sessionIdRef.current) {
2629
+ const firstUser = messagesRef.current.find((m) => m.role === "user");
2630
+ const firstText = typeof firstUser?.content === "string" ? firstUser.content : "session";
2631
+ sessionIdRef.current = makeSessionId(firstText);
2632
+ }
2633
+ try {
2634
+ await saveSession({
2635
+ id: sessionIdRef.current,
2636
+ cwd: process.cwd(),
2637
+ model: cfg.model,
2638
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2639
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2640
+ messages: messagesRef.current
2641
+ });
2642
+ } catch {
2643
+ }
2644
+ }, [cfg]);
2021
2645
  useInput2((inputChar, key) => {
2022
2646
  if (key.ctrl && inputChar === "c") {
2023
2647
  if (busy && activeControllerRef.current) {
2024
2648
  activeControllerRef.current.abort();
2649
+ setQueue([]);
2025
2650
  setEvents((e) => [...e, { kind: "info", key: mkKey(), text: "(interrupted)" }]);
2026
2651
  } else {
2027
2652
  exit();
2028
2653
  }
2654
+ return;
2655
+ }
2656
+ if (key.ctrl && inputChar === "r") {
2657
+ setShowReasoning((s) => !s);
2658
+ return;
2659
+ }
2660
+ if (key.shift && key.tab) {
2661
+ setMode((m) => {
2662
+ const nm = nextMode(m);
2663
+ setEvents((e) => [
2664
+ ...e,
2665
+ { kind: "info", key: mkKey(), text: `mode: ${nm}` }
2666
+ ]);
2667
+ return nm;
2668
+ });
2669
+ return;
2670
+ }
2671
+ if (key.ctrl && inputChar === "o") {
2672
+ setVerbose((v) => {
2673
+ const next = !v;
2674
+ setEvents((e) => [
2675
+ ...e,
2676
+ { kind: "info", key: mkKey(), text: `output: ${next ? "verbose (full tool results)" : "compact"}` }
2677
+ ]);
2678
+ return next;
2679
+ });
2680
+ return;
2029
2681
  }
2030
- if (key.ctrl && inputChar === "r") setShowReasoning((s) => !s);
2031
2682
  });
2032
2683
  const updateAssistant = useCallback(
2033
2684
  (id, patch) => {
@@ -2049,17 +2700,102 @@ function App({ initialCfg }) {
2049
2700
  },
2050
2701
  []
2051
2702
  );
2703
+ const runCompact = useCallback(async () => {
2704
+ if (!cfg) return;
2705
+ if (busy) {
2706
+ setEvents((e) => [...e, { kind: "info", key: mkKey(), text: "can't compact while model is running" }]);
2707
+ return;
2708
+ }
2709
+ setBusy(true);
2710
+ setEvents((e) => [...e, { kind: "info", key: mkKey(), text: "compacting conversation\u2026" }]);
2711
+ const controller = new AbortController();
2712
+ activeControllerRef.current = controller;
2713
+ try {
2714
+ const result = await compactMessages({
2715
+ accountId: cfg.accountId,
2716
+ apiToken: cfg.apiToken,
2717
+ model: cfg.model,
2718
+ messages: messagesRef.current,
2719
+ signal: controller.signal
2720
+ });
2721
+ if (result.replacedCount === 0) {
2722
+ setEvents((e) => [
2723
+ ...e,
2724
+ { kind: "info", key: mkKey(), text: "nothing to compact yet" }
2725
+ ]);
2726
+ } else {
2727
+ messagesRef.current = result.newMessages;
2728
+ setEvents((e) => [
2729
+ ...e,
2730
+ {
2731
+ kind: "info",
2732
+ key: mkKey(),
2733
+ text: `compacted ${result.replacedCount} messages into a summary`
2734
+ }
2735
+ ]);
2736
+ await saveSessionSafe();
2737
+ }
2738
+ } catch (e) {
2739
+ if (e.name !== "AbortError") {
2740
+ setEvents((es) => [
2741
+ ...es,
2742
+ { kind: "error", key: mkKey(), text: `compact failed: ${e.message}` }
2743
+ ]);
2744
+ }
2745
+ } finally {
2746
+ setBusy(false);
2747
+ activeControllerRef.current = null;
2748
+ }
2749
+ }, [cfg, busy, saveSessionSafe]);
2750
+ const openResumePicker = useCallback(async () => {
2751
+ const sessions = await listSessions(30);
2752
+ setResumeSessions(sessions);
2753
+ }, []);
2754
+ const handleResumePick = useCallback(
2755
+ async (picked) => {
2756
+ setResumeSessions(null);
2757
+ if (!picked) return;
2758
+ try {
2759
+ const file = await loadSession(picked.filePath);
2760
+ messagesRef.current = file.messages;
2761
+ sessionIdRef.current = file.id;
2762
+ setEvents([
2763
+ {
2764
+ kind: "info",
2765
+ key: mkKey(),
2766
+ text: `resumed session ${picked.id} (${picked.messageCount} msgs)`
2767
+ }
2768
+ ]);
2769
+ const userMsgs = file.messages.filter((m) => m.role === "user" && typeof m.content === "string").map((m) => m.content);
2770
+ if (userMsgs.length > 0) setHistory(userMsgs);
2771
+ setUsage(null);
2772
+ } catch (e) {
2773
+ setEvents((es) => [
2774
+ ...es,
2775
+ { kind: "error", key: mkKey(), text: `failed to load session: ${e.message}` }
2776
+ ]);
2777
+ }
2778
+ },
2779
+ []
2780
+ );
2052
2781
  const handleSlash = useCallback(
2053
2782
  (cmd) => {
2054
- const c = cmd.trim().toLowerCase();
2783
+ const raw = cmd.trim();
2784
+ const [head, ...rest] = raw.split(/\s+/);
2785
+ const c = (head ?? "").toLowerCase();
2786
+ const arg = rest.join(" ").trim().toLowerCase();
2055
2787
  if (c === "/exit" || c === "/quit") {
2056
2788
  exit();
2057
2789
  return true;
2058
2790
  }
2059
2791
  if (c === "/clear") {
2060
2792
  messagesRef.current = [messagesRef.current[0]];
2793
+ sessionIdRef.current = null;
2061
2794
  setEvents([{ kind: "info", key: mkKey(), text: "conversation cleared" }]);
2062
2795
  setUsage(null);
2796
+ setTasks([]);
2797
+ setTasksStartedAt(null);
2798
+ setTasksStartTokens(0);
2063
2799
  return true;
2064
2800
  }
2065
2801
  if (c === "/reasoning") {
@@ -2091,6 +2827,114 @@ function App({ initialCfg }) {
2091
2827
  ]);
2092
2828
  return true;
2093
2829
  }
2830
+ if (c === "/thinking" || c === "/effort") {
2831
+ if (!arg) {
2832
+ setEvents((e) => [
2833
+ ...e,
2834
+ {
2835
+ kind: "info",
2836
+ key: mkKey(),
2837
+ text: `current: ${effort} \xB7 ${EFFORT_DESCRIPTIONS[effort]}
2838
+ use: /thinking low | medium | high`
2839
+ }
2840
+ ]);
2841
+ return true;
2842
+ }
2843
+ if (arg === "low" || arg === "medium" || arg === "high") {
2844
+ setEffort(arg);
2845
+ if (cfg) void saveConfig({ ...cfg, reasoningEffort: arg }).catch(() => {
2846
+ });
2847
+ setEvents((e) => [
2848
+ ...e,
2849
+ {
2850
+ kind: "info",
2851
+ key: mkKey(),
2852
+ text: `thinking: ${arg} \xB7 ${EFFORT_DESCRIPTIONS[arg]}`
2853
+ }
2854
+ ]);
2855
+ return true;
2856
+ }
2857
+ setEvents((e) => [
2858
+ ...e,
2859
+ { kind: "info", key: mkKey(), text: "usage: /thinking low | medium | high" }
2860
+ ]);
2861
+ return true;
2862
+ }
2863
+ if (c === "/theme") {
2864
+ if (!arg) {
2865
+ setEvents((e) => [
2866
+ ...e,
2867
+ {
2868
+ kind: "info",
2869
+ key: mkKey(),
2870
+ text: `current: ${theme.name} \xB7 available: ${themeNames().join(", ")}`
2871
+ }
2872
+ ]);
2873
+ return true;
2874
+ }
2875
+ const next = resolveTheme(arg);
2876
+ if (next.name !== arg) {
2877
+ setEvents((e) => [
2878
+ ...e,
2879
+ { kind: "info", key: mkKey(), text: `unknown theme "${arg}" \u2014 available: ${themeNames().join(", ")}` }
2880
+ ]);
2881
+ return true;
2882
+ }
2883
+ setTheme(next);
2884
+ setCfg((c2) => c2 ? { ...c2, theme: next.name } : c2);
2885
+ if (cfg) void saveConfig({ ...cfg, theme: next.name }).catch(() => {
2886
+ });
2887
+ setEvents((e) => [
2888
+ ...e,
2889
+ { kind: "info", key: mkKey(), text: `theme: ${next.label}` }
2890
+ ]);
2891
+ return true;
2892
+ }
2893
+ if (c === "/mode") {
2894
+ if (!arg) {
2895
+ setEvents((e) => [
2896
+ ...e,
2897
+ { kind: "info", key: mkKey(), text: `current mode: ${mode} \xB7 use /mode edit|plan|auto or shift+tab` }
2898
+ ]);
2899
+ return true;
2900
+ }
2901
+ if (arg === "edit" || arg === "plan" || arg === "auto") {
2902
+ setMode(arg);
2903
+ setEvents((e) => [
2904
+ ...e,
2905
+ { kind: "info", key: mkKey(), text: `mode: ${arg}` }
2906
+ ]);
2907
+ return true;
2908
+ }
2909
+ setEvents((e) => [
2910
+ ...e,
2911
+ { kind: "info", key: mkKey(), text: "usage: /mode edit|plan|auto" }
2912
+ ]);
2913
+ return true;
2914
+ }
2915
+ if (c === "/plan") {
2916
+ setMode("plan");
2917
+ setEvents((e) => [...e, { kind: "info", key: mkKey(), text: "mode: plan" }]);
2918
+ return true;
2919
+ }
2920
+ if (c === "/auto") {
2921
+ setMode("auto");
2922
+ setEvents((e) => [...e, { kind: "info", key: mkKey(), text: "mode: auto" }]);
2923
+ return true;
2924
+ }
2925
+ if (c === "/edit") {
2926
+ setMode("edit");
2927
+ setEvents((e) => [...e, { kind: "info", key: mkKey(), text: "mode: edit" }]);
2928
+ return true;
2929
+ }
2930
+ if (c === "/resume") {
2931
+ void openResumePicker();
2932
+ return true;
2933
+ }
2934
+ if (c === "/compact") {
2935
+ void runCompact();
2936
+ return true;
2937
+ }
2094
2938
  if (c === "/update") {
2095
2939
  if (updateInfo?.hasUpdate) {
2096
2940
  setEvents((e) => [
@@ -2146,22 +2990,23 @@ function App({ initialCfg }) {
2146
2990
  {
2147
2991
  kind: "info",
2148
2992
  key: mkKey(),
2149
- text: "commands: /clear /reasoning /cost /model /update /logout /help /exit \xB7 keys: ctrl-r toggle reasoning, ctrl-c interrupt/exit"
2993
+ text: "commands:\n /mode edit|plan|auto switch mode (or shift+tab to cycle)\n /plan /auto /edit shortcuts for /mode\n /thinking low|med|high set reasoning effort (quality vs speed)\n /theme NAME dark, light, high-contrast\n /resume pick a past conversation\n /compact summarize old turns to free context\n /reasoning toggle show/hide model reasoning\n /clear clear current conversation\n /cost /model /update /logout /help /exit\nkeys: ctrl-c interrupt/exit \xB7 ctrl-r toggle reasoning \xB7 ctrl-o toggle verbose output \xB7 shift+tab cycle mode \xB7 \u2191/\u2193 history"
2150
2994
  }
2151
2995
  ]);
2152
2996
  return true;
2153
2997
  }
2154
2998
  return false;
2155
2999
  },
2156
- [cfg, exit, usage, updateInfo]
3000
+ [cfg, exit, usage, updateInfo, effort, theme, mode, openResumePicker, runCompact]
2157
3001
  );
2158
3002
  const processMessage = useCallback(
2159
- async (text) => {
3003
+ async (text, displayText) => {
2160
3004
  if (!cfg) return;
2161
3005
  const trimmed = text.trim();
2162
3006
  if (!trimmed) return;
2163
3007
  if (trimmed.startsWith("/") && handleSlash(trimmed)) return;
2164
- setEvents((e) => [...e, { kind: "user", key: mkKey(), text: trimmed }]);
3008
+ const display = displayText?.trim() || trimmed;
3009
+ setEvents((e) => [...e, { kind: "user", key: mkKey(), text: display }]);
2165
3010
  messagesRef.current.push({ role: "user", content: trimmed });
2166
3011
  setBusy(true);
2167
3012
  const controller = new AbortController();
@@ -2176,6 +3021,7 @@ function App({ initialCfg }) {
2176
3021
  executor: executorRef.current,
2177
3022
  cwd: process.cwd(),
2178
3023
  signal: controller.signal,
3024
+ reasoningEffort: effortRef.current,
2179
3025
  callbacks: {
2180
3026
  onAssistantStart: () => {
2181
3027
  const id = nextAssistantId++;
@@ -2226,11 +3072,41 @@ function App({ initialCfg }) {
2226
3072
  });
2227
3073
  },
2228
3074
  onUsage: (u) => setUsage(u),
3075
+ onTasks: (nextTasks) => {
3076
+ setTasks((prev) => {
3077
+ if (prev.length === 0 && nextTasks.length > 0) {
3078
+ setTasksStartedAt(Date.now());
3079
+ setTasksStartTokens(usage?.prompt_tokens ?? 0);
3080
+ }
3081
+ if (nextTasks.length === 0) {
3082
+ setTasksStartedAt(null);
3083
+ setTasksStartTokens(0);
3084
+ }
3085
+ return nextTasks;
3086
+ });
3087
+ },
2229
3088
  askPermission: (req) => new Promise((resolve2) => {
3089
+ if (modeRef.current === "auto") {
3090
+ resolve2("allow");
3091
+ return;
3092
+ }
3093
+ if (modeRef.current === "plan" && isBlockedInPlanMode(req.tool.name)) {
3094
+ setEvents((e) => [
3095
+ ...e,
3096
+ {
3097
+ kind: "info",
3098
+ key: mkKey(),
3099
+ text: `plan mode blocked ${req.tool.name}; exit plan mode to execute`
3100
+ }
3101
+ ]);
3102
+ resolve2("deny");
3103
+ return;
3104
+ }
2230
3105
  setPerm({ tool: req.tool, args: req.args, resolve: resolve2 });
2231
3106
  })
2232
3107
  }
2233
3108
  });
3109
+ await saveSessionSafe();
2234
3110
  } catch (e) {
2235
3111
  if (e.name === "AbortError") {
2236
3112
  setEvents((es) => [...es, { kind: "info", key: mkKey(), text: "(aborted)" }]);
@@ -2246,36 +3122,36 @@ function App({ initialCfg }) {
2246
3122
  activeControllerRef.current = null;
2247
3123
  }
2248
3124
  },
2249
- [cfg, handleSlash, updateAssistant, updateTool]
3125
+ [cfg, handleSlash, updateAssistant, updateTool, saveSessionSafe]
2250
3126
  );
2251
- useEffect2(() => {
3127
+ useEffect3(() => {
2252
3128
  if (!busy && queue.length > 0) {
2253
3129
  const next = queue[0];
2254
3130
  setQueue((q) => q.slice(1));
2255
- processMessage(next);
3131
+ processMessage(next.full, next.display);
2256
3132
  }
2257
3133
  }, [busy, queue, processMessage]);
2258
3134
  const submit = useCallback(
2259
- (text) => {
2260
- const trimmed = text.trim();
2261
- if (!trimmed) return;
3135
+ (full, display) => {
3136
+ const trimmedFull = full.trim();
3137
+ if (!trimmedFull) return;
3138
+ const trimmedDisplay = (display ?? full).trim() || trimmedFull;
3139
+ const historyEntry = trimmedDisplay;
2262
3140
  if (busy) {
2263
- setQueue((q) => [...q, trimmed]);
2264
- setHistory((h) => h.length > 0 && h[h.length - 1] === trimmed ? h : [...h, trimmed]);
3141
+ setQueue((q) => [...q, { full: trimmedFull, display: trimmedDisplay }]);
3142
+ setHistory((h) => h.length > 0 && h[h.length - 1] === historyEntry ? h : [...h, historyEntry]);
2265
3143
  setInput("");
2266
3144
  setHistoryIndex(-1);
2267
3145
  return;
2268
3146
  }
2269
- setHistory((h) => h.length > 0 && h[h.length - 1] === trimmed ? h : [...h, trimmed]);
3147
+ setHistory((h) => h.length > 0 && h[h.length - 1] === historyEntry ? h : [...h, historyEntry]);
2270
3148
  setInput("");
2271
3149
  setHistoryIndex(-1);
2272
- processMessage(trimmed);
3150
+ processMessage(trimmedFull, trimmedDisplay !== trimmedFull ? trimmedDisplay : void 0);
2273
3151
  },
2274
3152
  [busy, processMessage]
2275
3153
  );
2276
- useEffect2(() => {
2277
- }, [events]);
2278
- useEffect2(() => {
3154
+ useEffect3(() => {
2279
3155
  checkForUpdate().then((result) => {
2280
3156
  if (result.hasUpdate) {
2281
3157
  setUpdateInfo(result);
@@ -2290,8 +3166,24 @@ function App({ initialCfg }) {
2290
3166
  }
2291
3167
  });
2292
3168
  }, []);
3169
+ useEffect3(() => {
3170
+ if (usage && usage.prompt_tokens / CONTEXT_LIMIT >= AUTO_COMPACT_SUGGEST_PCT) {
3171
+ setEvents((e) => {
3172
+ const last = e[e.length - 1];
3173
+ if (last?.kind === "info" && last.text.startsWith("context ")) return e;
3174
+ return [
3175
+ ...e,
3176
+ {
3177
+ kind: "info",
3178
+ key: mkKey(),
3179
+ text: `context ${Math.round(usage.prompt_tokens / CONTEXT_LIMIT * 100)}% full \u2014 run /compact to summarize older turns`
3180
+ }
3181
+ ];
3182
+ });
3183
+ }
3184
+ }, [usage]);
2293
3185
  if (!cfg) {
2294
- return /* @__PURE__ */ jsx8(
3186
+ return /* @__PURE__ */ jsx10(
2295
3187
  Onboarding,
2296
3188
  {
2297
3189
  onDone: (newCfg) => {
@@ -2304,43 +3196,62 @@ function App({ initialCfg }) {
2304
3196
  }
2305
3197
  );
2306
3198
  }
2307
- return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
2308
- /* @__PURE__ */ jsx8(ChatView, { events, showReasoning }),
2309
- perm ? /* @__PURE__ */ jsx8(
3199
+ if (resumeSessions !== null) {
3200
+ return /* @__PURE__ */ jsx10(Box9, { flexDirection: "column", children: /* @__PURE__ */ jsx10(ResumePicker, { sessions: resumeSessions, onPick: handleResumePick, theme }) });
3201
+ }
3202
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
3203
+ /* @__PURE__ */ jsx10(ChatView, { events, showReasoning, theme, verbose }),
3204
+ perm ? /* @__PURE__ */ jsx10(
2310
3205
  PermissionModal,
2311
3206
  {
2312
3207
  tool: perm.tool,
2313
3208
  args: perm.args,
3209
+ theme,
2314
3210
  onDecide: (d) => {
2315
3211
  perm.resolve(d);
2316
3212
  setPerm(null);
2317
3213
  }
2318
3214
  }
2319
- ) : /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", marginTop: 1, children: [
2320
- queue.length > 0 && /* @__PURE__ */ jsx8(Box7, { flexDirection: "column", marginBottom: 1, children: queue.map((q, i) => /* @__PURE__ */ jsxs6(Text8, { color: "gray", dimColor: true, children: [
3215
+ ) : /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginTop: 1, children: [
3216
+ tasks.length > 0 && /* @__PURE__ */ jsx10(
3217
+ TaskList,
3218
+ {
3219
+ tasks,
3220
+ theme,
3221
+ startedAt: tasksStartedAt,
3222
+ tokensDelta: Math.max(0, (usage?.prompt_tokens ?? 0) - tasksStartTokens)
3223
+ }
3224
+ ),
3225
+ queue.length > 0 && /* @__PURE__ */ jsx10(Box9, { flexDirection: "column", marginBottom: 1, children: queue.map((q, i) => /* @__PURE__ */ jsxs9(Text10, { color: theme.queue.color, dimColor: theme.queue.dim, children: [
2321
3226
  "\u23F3 ",
2322
- q
3227
+ q.display
2323
3228
  ] }, `queue_${i}`)) }),
2324
- /* @__PURE__ */ jsx8(
3229
+ /* @__PURE__ */ jsx10(
2325
3230
  StatusBar,
2326
3231
  {
2327
3232
  model: cfg.model,
2328
3233
  usage,
2329
3234
  thinking: busy,
2330
- hint: busy ? "ctrl-c to interrupt" : "enter to send \xB7 /help"
3235
+ hint: busy ? "ctrl-c to interrupt \xB7 type to queue next" : "enter to send \xB7 /help \xB7 shift+tab cycles mode",
3236
+ theme,
3237
+ mode,
3238
+ effort,
3239
+ contextLimit: CONTEXT_LIMIT
2331
3240
  }
2332
3241
  ),
2333
- /* @__PURE__ */ jsx8(Box7, { children: busy ? /* @__PURE__ */ jsxs6(Box7, { children: [
2334
- /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: /* @__PURE__ */ jsx8(Spinner2, { type: "dots" }) }),
2335
- /* @__PURE__ */ jsx8(Text8, { color: "gray", children: " working\u2026" })
2336
- ] }) : /* @__PURE__ */ jsxs6(Box7, { children: [
2337
- /* @__PURE__ */ jsx8(Text8, { color: "cyan", children: "\u203A " }),
2338
- /* @__PURE__ */ jsx8(
3242
+ busy && /* @__PURE__ */ jsxs9(Box9, { children: [
3243
+ /* @__PURE__ */ jsx10(Text10, { color: theme.spinner, children: /* @__PURE__ */ jsx10(Spinner2, { type: "dots" }) }),
3244
+ /* @__PURE__ */ jsx10(Text10, { color: theme.info.color, dimColor: theme.info.dim, children: " working\u2026" })
3245
+ ] }),
3246
+ /* @__PURE__ */ jsxs9(Box9, { children: [
3247
+ /* @__PURE__ */ jsx10(Text10, { color: theme.user, children: busy ? "\u23F3 " : "\u203A " }),
3248
+ /* @__PURE__ */ jsx10(
2339
3249
  CustomTextInput,
2340
3250
  {
2341
3251
  value: input,
2342
3252
  onChange: setInput,
2343
3253
  onSubmit: submit,
3254
+ enablePaste: true,
2344
3255
  onHistoryUp: () => {
2345
3256
  if (history.length === 0) return;
2346
3257
  if (historyIndex === -1) {
@@ -2367,7 +3278,7 @@ function App({ initialCfg }) {
2367
3278
  },
2368
3279
  onClearQueueItem: (text) => {
2369
3280
  setQueue((q) => {
2370
- const idx = q.indexOf(text);
3281
+ const idx = q.findIndex((item) => item.display === text);
2371
3282
  if (idx >= 0) {
2372
3283
  const next = [...q];
2373
3284
  next.splice(idx, 1);
@@ -2378,31 +3289,44 @@ function App({ initialCfg }) {
2378
3289
  }
2379
3290
  }
2380
3291
  )
2381
- ] }) })
3292
+ ] })
2382
3293
  ] })
2383
3294
  ] });
2384
3295
  }
2385
3296
  async function renderApp(cfg) {
2386
- const instance = render(/* @__PURE__ */ jsx8(App, { initialCfg: cfg }));
3297
+ const instance = render(/* @__PURE__ */ jsx10(App, { initialCfg: cfg }));
2387
3298
  await instance.waitUntilExit();
2388
3299
  }
2389
- var nextAssistantId, nextKey, mkKey;
3300
+ var CONTEXT_LIMIT, AUTO_COMPACT_SUGGEST_PCT, nextAssistantId, nextKey, mkKey, EFFORT_DESCRIPTIONS;
2390
3301
  var init_app = __esm({
2391
3302
  "src/app.tsx"() {
2392
3303
  "use strict";
2393
3304
  init_loop();
2394
3305
  init_system_prompt();
3306
+ init_compact();
2395
3307
  init_executor();
2396
3308
  init_chat();
2397
3309
  init_status();
2398
3310
  init_permission();
3311
+ init_resume_picker();
3312
+ init_task_list();
2399
3313
  init_text_input();
2400
3314
  init_update_check();
2401
3315
  init_onboarding();
2402
3316
  init_config();
3317
+ init_theme();
3318
+ init_mode();
3319
+ init_sessions();
3320
+ CONTEXT_LIMIT = 262e3;
3321
+ AUTO_COMPACT_SUGGEST_PCT = 0.8;
2403
3322
  nextAssistantId = 1;
2404
3323
  nextKey = 1;
2405
3324
  mkKey = () => `evt_${nextKey++}`;
3325
+ EFFORT_DESCRIPTIONS = {
3326
+ low: "low \u2014 fastest; lightest reasoning. Best for simple Q&A, small edits, quick coordination.",
3327
+ medium: "medium \u2014 balanced (default). Solid quality on most edits, fast on trivial prompts.",
3328
+ high: "high \u2014 deepest reasoning; slowest. Best for complex debugging, architecture, multi-file refactors."
3329
+ };
2406
3330
  }
2407
3331
  });
2408
3332