@pikaa-ai/pikaa 0.2.2 → 0.2.4

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/cli.js CHANGED
@@ -127,6 +127,7 @@ class DefaultModelClientSession {
127
127
  model,
128
128
  messages,
129
129
  stream: true,
130
+ stream_options: { include_usage: true },
130
131
  temperature: params.temperature ?? 0.2
131
132
  };
132
133
  if (toolsPayload && toolsPayload.length > 0) {
@@ -175,6 +176,7 @@ class DefaultModelClientSession {
175
176
  const reader = response.body.getReader();
176
177
  const decoder = new TextDecoder;
177
178
  let buffer = "";
179
+ let usageMetrics = {};
178
180
  const pendingToolCalls = new Map;
179
181
  try {
180
182
  while (true) {
@@ -204,11 +206,23 @@ class DefaultModelClientSession {
204
206
  };
205
207
  }
206
208
  pendingToolCalls.clear();
207
- yield { type: "done" };
209
+ yield {
210
+ type: "done",
211
+ inputTokens: usageMetrics.inputTokens,
212
+ outputTokens: usageMetrics.outputTokens,
213
+ totalTokens: usageMetrics.totalTokens
214
+ };
208
215
  return;
209
216
  }
210
217
  try {
211
218
  const data = JSON.parse(dataStr);
219
+ if (data.usage) {
220
+ usageMetrics = {
221
+ inputTokens: data.usage.prompt_tokens,
222
+ outputTokens: data.usage.completion_tokens,
223
+ totalTokens: data.usage.total_tokens
224
+ };
225
+ }
212
226
  const choice = data.choices?.[0];
213
227
  if (!choice)
214
228
  continue;
@@ -454,12 +468,16 @@ function buildSystemPrompt(params) {
454
468
  const sections = [];
455
469
  sections.push(params.basePrompt || "You are Groupy, an expert autonomous AI coding assistant. You think step-by-step, act surgically, and write clean, correct code.");
456
470
  sections.push([
457
- "## Operational Guidelines",
458
- "1. Think before acting. Understand the task and the code it touches before making edits.",
459
- "2. Simplicity first: write the minimum code that solves the problem. No unnecessary abstractions.",
460
- "3. Use `apply_patch` for surgical code replacements rather than overwriting whole files.",
461
- "4. Execute shell commands with `shell` to inspect environment, run tests, or build targets.",
462
- "5. Always verify your changes with tests or execution checks."
471
+ "## Editing Constraints & Guidelines",
472
+ "- Use `apply_patch` for surgical single-file edits. TargetContent must match existing file content exactly.",
473
+ "- Use `write_file` for creating new files or when completely replacing the full content of a file.",
474
+ "- Use `read_file` to inspect files and `grep_search` / `find_files` to discover symbols and locate files across the project.",
475
+ "- NEVER create temporary scripts, scratch files, or chunk files (e.g. `_tmp_*.ps1`, `_tmp_*.txt`, `split_*.py`) in the workspace to manipulate, split, or read files.",
476
+ "- NEVER execute shell or PowerShell scripts as a workaround for reading, writing, or editing text files.",
477
+ "- Use the `shell` tool ONLY for running tests, build targets, package installations, or checking environment/git status.",
478
+ "- You may be in a dirty git worktree. NEVER revert existing changes made by the user.",
479
+ "- NEVER use destructive commands like `git reset --hard` or `git checkout --`.",
480
+ "- Be concise, direct, and act surgically. Write clean, correct code with minimal necessary modifications."
463
481
  ].join(`
464
482
  `));
465
483
  if (params.memoriesPrompt) {
@@ -523,6 +541,8 @@ async function runTurn(session, turnContext, input) {
523
541
  skillsPrompt
524
542
  });
525
543
  let iteration = 0;
544
+ let accumulatedInputTokens = 0;
545
+ let accumulatedOutputTokens = 0;
526
546
  const clientSession = session.modelClient.newSession();
527
547
  try {
528
548
  while (iteration < turnContext.maxIterations) {
@@ -532,6 +552,8 @@ async function runTurn(session, turnContext, input) {
532
552
  iteration++;
533
553
  let currentAgentText = "";
534
554
  const toolCallRequests = [];
555
+ let iterInputTokens = Math.ceil((effectiveSystemPrompt.length + JSON.stringify(session.getHistory()).length) / 4);
556
+ let iterOutputTokens = 0;
535
557
  const stream = clientSession.stream({
536
558
  model: turnContext.model,
537
559
  systemPrompt: effectiveSystemPrompt,
@@ -558,10 +580,20 @@ async function runTurn(session, turnContext, input) {
558
580
  });
559
581
  } else if (chunk.type === "tool_call") {
560
582
  toolCallRequests.push(chunk);
583
+ } else if (chunk.type === "done") {
584
+ if (chunk.inputTokens !== undefined)
585
+ iterInputTokens = chunk.inputTokens;
586
+ if (chunk.outputTokens !== undefined)
587
+ iterOutputTokens = chunk.outputTokens;
561
588
  } else if (chunk.type === "error") {
562
589
  throw chunk.error;
563
590
  }
564
591
  }
592
+ if (iterOutputTokens === 0) {
593
+ iterOutputTokens = Math.ceil((currentAgentText.length + JSON.stringify(toolCallRequests).length) / 4);
594
+ }
595
+ accumulatedInputTokens += iterInputTokens;
596
+ accumulatedOutputTokens += iterOutputTokens;
565
597
  if (currentAgentText.trim()) {
566
598
  const agentItem = {
567
599
  id: `msg_agent_${Date.now()}`,
@@ -602,6 +634,7 @@ async function runTurn(session, turnContext, input) {
602
634
  cwd: turnContext.environment.cwd,
603
635
  turnId,
604
636
  signal,
637
+ execPolicy: session.execPolicy,
605
638
  requestApproval: async (description, command) => {
606
639
  const approvalId = `appr_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
607
640
  return session.requestApproval({
@@ -639,9 +672,16 @@ async function runTurn(session, turnContext, input) {
639
672
  }
640
673
  break;
641
674
  }
675
+ const totalContextTokens = estimateTotalTokens(session.getHistory()) + Math.ceil(effectiveSystemPrompt.length / 4);
676
+ const maxContextTokens = 128000;
642
677
  session.emitEvent({
643
678
  type: "TurnCompleted",
644
- turnId
679
+ turnId,
680
+ inputTokens: accumulatedInputTokens,
681
+ outputTokens: accumulatedOutputTokens,
682
+ totalTokens: accumulatedInputTokens + accumulatedOutputTokens,
683
+ contextTokens: totalContextTokens,
684
+ maxContextTokens
645
685
  });
646
686
  } catch (error) {
647
687
  const isAborted = error instanceof TurnAbortedError || signal.aborted;
@@ -725,6 +765,40 @@ async function submissionLoop(session, queue) {
725
765
  }
726
766
  }
727
767
 
768
+ // src/security/exec-policy.ts
769
+ class ExecPolicy {
770
+ rules = [];
771
+ constructor() {
772
+ this.initDefaultRules();
773
+ }
774
+ initDefaultRules() {
775
+ this.addRule(/^(git\s+(status|log|diff|branch|show|rev-parse))/i, "allow", "Safe git query");
776
+ this.addRule(/^(ls|dir|cat|type|grep|rg|find|pwd|echo|head|tail|wc|which|where)\b/i, "allow", "Safe read-only shell command");
777
+ this.addRule(/^(bun\s+(test|--version|-v)|npm\s+(test|--version|-v)|node\s+-v)\b/i, "allow", "Testing & runtime check");
778
+ this.addRule(/^(rm|del|rmdir|format|mkfs)\b/i, "prompt", "Destructive file removal");
779
+ this.addRule(/^(git\s+(push|reset\s+--hard|clean\s+-fd|rebase))\b/i, "prompt", "Destructive git operation");
780
+ this.addRule(/^(curl|wget|fetch|ssh|scp|ftp)\b/i, "prompt", "Network / remote transfer");
781
+ }
782
+ addRule(pattern, decision, description) {
783
+ this.rules.unshift({ pattern, decision, description });
784
+ }
785
+ evaluate(command) {
786
+ const trimmed = command.trim();
787
+ for (const rule of this.rules) {
788
+ if (rule.pattern.test(trimmed)) {
789
+ return {
790
+ decision: rule.decision,
791
+ reason: rule.description
792
+ };
793
+ }
794
+ }
795
+ return {
796
+ decision: "prompt",
797
+ reason: "Command is not in the automatic allowlist"
798
+ };
799
+ }
800
+ }
801
+
728
802
  // src/session/session.ts
729
803
  class Session {
730
804
  threadId;
@@ -735,6 +809,7 @@ class Session {
735
809
  tools;
736
810
  skillsLoader;
737
811
  memoryStore;
812
+ execPolicy;
738
813
  history = [];
739
814
  activeTurn = null;
740
815
  status = "idle";
@@ -752,6 +827,7 @@ class Session {
752
827
  this.tools = options.tools || new ToolRouter;
753
828
  this.skillsLoader = options.skillsLoader;
754
829
  this.memoryStore = options.memoryStore;
830
+ this.execPolicy = options.execPolicy || new ExecPolicy;
755
831
  this.history = options.initialHistory ? [...options.initialHistory] : [];
756
832
  if (options.onEvent) {
757
833
  this.eventListeners.push(options.onEvent);
@@ -1001,40 +1077,6 @@ var applyPatchTool = {
1001
1077
  }
1002
1078
  }
1003
1079
  };
1004
- // src/security/exec-policy.ts
1005
- class ExecPolicy {
1006
- rules = [];
1007
- constructor() {
1008
- this.initDefaultRules();
1009
- }
1010
- initDefaultRules() {
1011
- this.addRule(/^(git\s+(status|log|diff|branch|show|rev-parse))/i, "allow", "Safe git query");
1012
- this.addRule(/^(ls|dir|cat|grep|rg|find|pwd|echo|head|tail|wc|which|where)\b/i, "allow", "Safe read-only shell command");
1013
- this.addRule(/^(bun\s+(test|--version|-v)|npm\s+(test|--version|-v)|node\s+-v)\b/i, "allow", "Testing & runtime check");
1014
- this.addRule(/^(rm|del|rmdir|format|mkfs)\b/i, "prompt", "Destructive file removal");
1015
- this.addRule(/^(git\s+(push|reset\s+--hard|clean\s+-fd|rebase))\b/i, "prompt", "Destructive git operation");
1016
- this.addRule(/^(curl|wget|fetch|ssh|scp|ftp)\b/i, "prompt", "Network / remote transfer");
1017
- }
1018
- addRule(pattern, decision, description) {
1019
- this.rules.unshift({ pattern, decision, description });
1020
- }
1021
- evaluate(command) {
1022
- const trimmed = command.trim();
1023
- for (const rule of this.rules) {
1024
- if (rule.pattern.test(trimmed)) {
1025
- return {
1026
- decision: rule.decision,
1027
- reason: rule.description
1028
- };
1029
- }
1030
- }
1031
- return {
1032
- decision: "prompt",
1033
- reason: "Command is not in the automatic allowlist"
1034
- };
1035
- }
1036
- }
1037
-
1038
1080
  // src/tools/handlers/shell.ts
1039
1081
  function createShellTool(policy = new ExecPolicy) {
1040
1082
  return {
@@ -1059,7 +1101,8 @@ function createShellTool(policy = new ExecPolicy) {
1059
1101
  if (!command) {
1060
1102
  return { output: "Error: 'command' argument cannot be empty", isError: true };
1061
1103
  }
1062
- const policyDecision = policy.evaluate(command);
1104
+ const activePolicy = ctx.execPolicy || policy;
1105
+ const policyDecision = activePolicy.evaluate(command);
1063
1106
  if (policyDecision.decision === "deny") {
1064
1107
  return {
1065
1108
  output: `Error: Command execution denied by policy: ${policyDecision.reason}`,
@@ -3904,7 +3947,7 @@ class AuthClient {
3904
3947
  </head>
3905
3948
  <body>
3906
3949
  <div class="box">
3907
- <h1>\u26A1 Authentication Successful!</h1>
3950
+ <h1>Authentication Successful!</h1>
3908
3951
  <p>You have successfully logged in to Groupy CLI. You can close this window and return to your terminal.</p>
3909
3952
  </div>
3910
3953
  </body>
@@ -4015,12 +4058,29 @@ var style = {
4015
4058
  };
4016
4059
 
4017
4060
  // src/cli/ui/spinner.ts
4061
+ function formatDuration(ms) {
4062
+ const totalSec = Math.floor(ms / 1000);
4063
+ if (totalSec < 60) {
4064
+ const sec = (ms / 1000).toFixed(1);
4065
+ return `${sec}s`;
4066
+ }
4067
+ const minutes = Math.floor(totalSec / 60);
4068
+ const seconds = totalSec % 60;
4069
+ if (minutes < 60) {
4070
+ return `${minutes}m ${seconds}s`;
4071
+ }
4072
+ const hours = Math.floor(minutes / 60);
4073
+ const remMinutes = minutes % 60;
4074
+ return `${hours}h ${remMinutes}m ${seconds}s`;
4075
+ }
4076
+
4018
4077
  class LiveSpinner {
4019
4078
  frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
4020
4079
  frameIndex = 0;
4021
4080
  timer = null;
4022
4081
  currentMessage = "";
4023
4082
  isSpinning = false;
4083
+ startTime = 0;
4024
4084
  start(message = "Thinking...") {
4025
4085
  if (this.isSpinning) {
4026
4086
  this.update(message);
@@ -4028,6 +4088,7 @@ class LiveSpinner {
4028
4088
  }
4029
4089
  this.isSpinning = true;
4030
4090
  this.currentMessage = message;
4091
+ this.startTime = performance.now();
4031
4092
  this.frameIndex = 0;
4032
4093
  this.render();
4033
4094
  this.timer = setInterval(() => {
@@ -4046,7 +4107,10 @@ class LiveSpinner {
4046
4107
  }
4047
4108
  render() {
4048
4109
  const frame = this.frames[this.frameIndex];
4049
- const output = `\r\x1B[K${c.cyan}${frame}${c.reset} ${c.dim}${this.currentMessage}${c.reset}`;
4110
+ const elapsedMs = performance.now() - this.startTime;
4111
+ const elapsedStr = formatDuration(elapsedMs);
4112
+ const timeBadge = style.dim(`(${elapsedStr})`);
4113
+ const output = `\r\x1B[K ${c.cyan}${frame}${c.reset} ${c.dim}${this.currentMessage}${c.reset} ${timeBadge}`;
4050
4114
  process.stdout.write(output);
4051
4115
  }
4052
4116
  stop(finalMessage, success = true) {
@@ -4078,30 +4142,271 @@ class LiveSpinner {
4078
4142
  }
4079
4143
  }
4080
4144
 
4145
+ // src/cli/ui/diff.ts
4146
+ var ESC2 = "\x1B[";
4147
+ var R = `${ESC2}0m`;
4148
+ var fg = {
4149
+ lineNum: `${ESC2}38;2;120;120;130m`,
4150
+ gutterDel: `${ESC2}38;2;210;90;90m`,
4151
+ gutterAdd: `${ESC2}38;2;80;200;120m`,
4152
+ textDel: `${ESC2}38;2;235;150;150m`,
4153
+ textAdd: `${ESC2}38;2;140;230;160m`,
4154
+ ctx: `${ESC2}38;2;180;180;195m`
4155
+ };
4156
+ var bg = {
4157
+ del: `${ESC2}48;2;60;20;25m`,
4158
+ add: `${ESC2}48;2;18;55;28m`,
4159
+ wordDel: `${ESC2}48;2;135;40;45m`,
4160
+ wordAdd: `${ESC2}48;2;30;115;50m`
4161
+ };
4162
+ function tokenize(line) {
4163
+ return line.match(/\w+|\s+|[^\w\s]+/g) ?? [];
4164
+ }
4165
+ function lcsTokenDiff(oldTokens, newTokens) {
4166
+ const m = oldTokens.length;
4167
+ const n = newTokens.length;
4168
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
4169
+ for (let i2 = 1;i2 <= m; i2++) {
4170
+ for (let j2 = 1;j2 <= n; j2++) {
4171
+ dp[i2][j2] = oldTokens[i2 - 1] === newTokens[j2 - 1] ? dp[i2 - 1][j2 - 1] + 1 : Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
4172
+ }
4173
+ }
4174
+ const ops = [];
4175
+ let i = m, j = n;
4176
+ while (i > 0 || j > 0) {
4177
+ if (i > 0 && j > 0 && oldTokens[i - 1] === newTokens[j - 1]) {
4178
+ ops.push({ same: oldTokens[i - 1] });
4179
+ i--;
4180
+ j--;
4181
+ } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
4182
+ ops.push({ add: newTokens[j - 1] });
4183
+ j--;
4184
+ } else {
4185
+ ops.push({ del: oldTokens[i - 1] });
4186
+ i--;
4187
+ }
4188
+ }
4189
+ return ops.reverse();
4190
+ }
4191
+ function visibleLen(s) {
4192
+ return s.replace(/\x1b\[[0-9;]*m/g, "").length;
4193
+ }
4194
+ function padToWidth(rendered, bgColor, termWidth) {
4195
+ const pad = Math.max(0, termWidth - visibleLen(rendered));
4196
+ return rendered + bgColor + " ".repeat(pad) + R;
4197
+ }
4198
+ function gutterWidth(maxLine) {
4199
+ return Math.max(1, String(maxLine).length);
4200
+ }
4201
+ function renderCtxRow(text, oldLine, newLine, numWidth, termWidth) {
4202
+ const oldNum = String(oldLine).padStart(numWidth);
4203
+ const newNum = String(newLine).padStart(numWidth);
4204
+ const gutter = `${fg.lineNum}${oldNum}${R} ${fg.lineNum}${newNum}${R} `;
4205
+ const body = `${fg.ctx}${text}${R}`;
4206
+ const raw = gutter + body;
4207
+ const pad = Math.max(0, termWidth - visibleLen(raw));
4208
+ return raw + " ".repeat(pad);
4209
+ }
4210
+ function renderDelRow(text, oldLine, numWidth, pairText, termWidth) {
4211
+ const num = String(oldLine).padStart(numWidth);
4212
+ const gutter = `${bg.del}${fg.lineNum}${num}${R}${bg.del}${fg.gutterDel} - ${R}`;
4213
+ let body;
4214
+ if (pairText !== undefined) {
4215
+ const ops = lcsTokenDiff(tokenize(text), tokenize(pairText));
4216
+ body = ops.map((op) => {
4217
+ if (op.same)
4218
+ return `${bg.del}${fg.textDel}${op.same}`;
4219
+ if (op.del)
4220
+ return `${bg.wordDel}${fg.textDel}${op.del}`;
4221
+ return "";
4222
+ }).join("") + R;
4223
+ } else {
4224
+ body = `${bg.del}${fg.textDel}${text}${R}`;
4225
+ }
4226
+ return padToWidth(gutter + body, bg.del, termWidth);
4227
+ }
4228
+ function renderAddRow(text, newLine, numWidth, pairText, termWidth) {
4229
+ const num = String(newLine).padStart(numWidth);
4230
+ const gutter = `${bg.add}${fg.lineNum}${num}${R}${bg.add}${fg.gutterAdd} + ${R}`;
4231
+ let body;
4232
+ if (pairText !== undefined) {
4233
+ const ops = lcsTokenDiff(tokenize(pairText), tokenize(text));
4234
+ body = ops.map((op) => {
4235
+ if (op.same)
4236
+ return `${bg.add}${fg.textAdd}${op.same}`;
4237
+ if (op.add)
4238
+ return `${bg.wordAdd}${fg.textAdd}${op.add}`;
4239
+ return "";
4240
+ }).join("") + R;
4241
+ } else {
4242
+ body = `${bg.add}${fg.textAdd}${text}${R}`;
4243
+ }
4244
+ return padToWidth(gutter + body, bg.add, termWidth);
4245
+ }
4246
+ function renderDiff(lines, opts = {}) {
4247
+ const termWidth = opts.termWidth ?? process.stdout.columns ?? 80;
4248
+ const maxLineNum = lines.reduce((m, l) => Math.max(m, l.oldLine ?? 0, l.newLine ?? 0), 0);
4249
+ const numWidth = gutterWidth(maxLineNum);
4250
+ const out = [];
4251
+ if (opts.filePath) {
4252
+ const headerText = ` ${opts.filePath}`;
4253
+ const padded = headerText.padEnd(termWidth);
4254
+ out.push(`${ESC2}48;2;30;30;40m${ESC2}38;2;200;180;255m${ESC2}1m${padded}${R}`);
4255
+ }
4256
+ let i = 0;
4257
+ while (i < lines.length) {
4258
+ const line = lines[i];
4259
+ if (line.kind === "ctx") {
4260
+ if (line.oldLine === undefined) {
4261
+ const sep = " \u22EF".padEnd(termWidth);
4262
+ out.push(`${ESC2}38;2;100;100;120m${sep}${R}`);
4263
+ } else {
4264
+ out.push(renderCtxRow(line.text, line.oldLine, line.newLine, numWidth, termWidth));
4265
+ }
4266
+ i++;
4267
+ continue;
4268
+ }
4269
+ const dels = [];
4270
+ const adds = [];
4271
+ while (i < lines.length && lines[i].kind === "del")
4272
+ dels.push(lines[i++]);
4273
+ while (i < lines.length && lines[i].kind === "add")
4274
+ adds.push(lines[i++]);
4275
+ const pairCount = Math.min(dels.length, adds.length);
4276
+ for (let p = 0;p < pairCount; p++) {
4277
+ out.push(renderDelRow(dels[p].text, dels[p].oldLine, numWidth, adds[p].text, termWidth));
4278
+ out.push(renderAddRow(adds[p].text, adds[p].newLine, numWidth, dels[p].text, termWidth));
4279
+ }
4280
+ for (let p = pairCount;p < dels.length; p++) {
4281
+ out.push(renderDelRow(dels[p].text, dels[p].oldLine, numWidth, undefined, termWidth));
4282
+ }
4283
+ for (let p = pairCount;p < adds.length; p++) {
4284
+ out.push(renderAddRow(adds[p].text, adds[p].newLine, numWidth, undefined, termWidth));
4285
+ }
4286
+ }
4287
+ return out.join(`
4288
+ `);
4289
+ }
4290
+ function parsePatch(oldSrc, newSrc, contextLines = 3) {
4291
+ const oldLines = oldSrc.split(`
4292
+ `);
4293
+ const newLines = newSrc.split(`
4294
+ `);
4295
+ const m = oldLines.length;
4296
+ const n = newLines.length;
4297
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
4298
+ for (let i = 1;i <= m; i++) {
4299
+ for (let j = 1;j <= n; j++) {
4300
+ dp[i][j] = oldLines[i - 1] === newLines[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]);
4301
+ }
4302
+ }
4303
+ const ops = [];
4304
+ let oi = m, ni = n;
4305
+ while (oi > 0 || ni > 0) {
4306
+ if (oi > 0 && ni > 0 && oldLines[oi - 1] === newLines[ni - 1]) {
4307
+ ops.push({ kind: "same", oi: oi - 1, ni: ni - 1 });
4308
+ oi--;
4309
+ ni--;
4310
+ } else if (ni > 0 && (oi === 0 || dp[oi][ni - 1] >= dp[oi - 1][ni])) {
4311
+ ops.push({ kind: "add", ni: ni - 1 });
4312
+ ni--;
4313
+ } else {
4314
+ ops.push({ kind: "del", oi: oi - 1 });
4315
+ oi--;
4316
+ }
4317
+ }
4318
+ ops.reverse();
4319
+ const changed = new Set;
4320
+ ops.forEach((op, idx) => {
4321
+ if (op.kind !== "same")
4322
+ changed.add(idx);
4323
+ });
4324
+ const visible = new Set;
4325
+ changed.forEach((idx) => {
4326
+ for (let k = Math.max(0, idx - contextLines);k <= Math.min(ops.length - 1, idx + contextLines); k++) {
4327
+ visible.add(k);
4328
+ }
4329
+ });
4330
+ const result = [];
4331
+ let prevVisible = -2;
4332
+ for (let idx = 0;idx < ops.length; idx++) {
4333
+ if (!visible.has(idx))
4334
+ continue;
4335
+ if (prevVisible !== -2 && idx > prevVisible + 1) {
4336
+ result.push({ kind: "ctx", text: "\u22EF", oldLine: undefined, newLine: undefined });
4337
+ }
4338
+ prevVisible = idx;
4339
+ const op = ops[idx];
4340
+ if (op.kind === "same") {
4341
+ result.push({ kind: "ctx", oldLine: op.oi + 1, newLine: op.ni + 1, text: oldLines[op.oi] });
4342
+ } else if (op.kind === "del") {
4343
+ result.push({ kind: "del", oldLine: op.oi + 1, text: oldLines[op.oi] });
4344
+ } else {
4345
+ result.push({ kind: "add", newLine: op.ni + 1, text: newLines[op.ni] });
4346
+ }
4347
+ }
4348
+ return result;
4349
+ }
4350
+
4081
4351
  // src/cli/ui/formatter.ts
4082
4352
  function renderGroupyBanner(info) {
4083
4353
  CliFormatter.printBanner(info);
4084
4354
  }
4085
- function formatToolCard(toolName, args, output, isError = false) {
4086
- CliFormatter.formatToolCall(toolName, args);
4087
- if (output !== undefined) {
4088
- CliFormatter.formatToolOutput(output, isError);
4355
+ function formatTurnSummary(metrics) {
4356
+ CliFormatter.formatTurnSummary(metrics);
4357
+ }
4358
+ function formatTaskStepStart(step, toolName, args) {
4359
+ const argsSummary = Object.entries(args).map(([k, v]) => {
4360
+ const valStr = typeof v === "string" ? `"${v.length > 35 ? v.slice(0, 32) + "..." : v}"` : JSON.stringify(v);
4361
+ return `${style.dim(k)}=${style.cyan(valStr)}`;
4362
+ }).join(" ");
4363
+ console.log(` ${style.brand("\u280B")} ${style.bold(`[${step}]`)} ${style.cyan(toolName)} ${argsSummary}`);
4364
+ }
4365
+ function formatTaskStepFinish(step, toolName, args, output, isError = false) {
4366
+ const icon = isError ? style.red("\u2717") : style.green("\u2714");
4367
+ const toolNameDisplay = style.bold(toolName);
4368
+ if (toolName === "apply_patch" && typeof args.targetContent === "string" && typeof args.replacementContent === "string") {
4369
+ const targetFile = args.path ? String(args.path) : undefined;
4370
+ console.log(` ${icon} ${style.dim(`[${step}]`)} ${toolNameDisplay} ${style.dim(targetFile || "")}`);
4371
+ CliFormatter.formatPatchDiff(targetFile, args.targetContent, args.replacementContent);
4372
+ return;
4373
+ }
4374
+ let summary = "";
4375
+ if (output) {
4376
+ const trimmed = output.trim();
4377
+ const lines = trimmed.split(`
4378
+ `);
4379
+ if (lines.length === 1 && lines[0].length <= 60) {
4380
+ summary = ` ${style.dim("\u21B3")} ${style.dim(lines[0])}`;
4381
+ } else if (toolName === "read_file" || toolName === "read_file_range") {
4382
+ summary = ` ${style.dim(`\u21B3 (${lines.length} lines read)`)}`;
4383
+ } else if (toolName === "grep_search" || toolName === "find_files") {
4384
+ summary = ` ${style.dim(`\u21B3 (${lines.length} items found)`)}`;
4385
+ } else {
4386
+ summary = ` ${style.dim(`\u21B3 (${lines.length} lines output)`)}`;
4387
+ }
4388
+ }
4389
+ console.log(` ${icon} ${style.dim(`[${step}]`)} ${toolNameDisplay}${summary}`);
4390
+ if (isError && output) {
4391
+ const firstLine = output.trim().split(`
4392
+ `)[0] || output;
4393
+ console.log(` ${style.red(firstLine.slice(0, 120))}`);
4089
4394
  }
4090
4395
  }
4091
-
4092
4396
  class CliFormatter {
4093
4397
  static printBanner(info) {
4094
4398
  const b = c.brandBold;
4095
4399
  const r = c.reset;
4096
4400
  const g = c.dim;
4097
4401
  const t = c.bold;
4402
+ const userDisplay = info.user ? style.cyan(info.user) : style.dim("Guest");
4098
4403
  console.log();
4099
4404
  console.log(` ${b} \u2584\u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2584\u2584${r}`);
4100
4405
  console.log(` ${b} \u2584\u2588\u2588\u2588\u2580\u2580 \u2580\u2580\u2588\u2588\u2588\u2584${r}`);
4101
4406
  console.log(` ${b} \u2584\u2588\u2588\u2580 \u2580\u2588\u2588\u2584${r} ${t}PIKAA AGENT${r}`);
4102
4407
  console.log(` ${b} \u2588\u2588\u2588 \u2584\u2584\u2588\u2588\u2588\u2588\u2584\u2584 \u2588\u2588\u2588${r} ${g}Autonomous Coding Engine${r}`);
4103
- console.log(` ${b}\u2588\u2588\u2588 \u2584\u2588\u2588\u2580 \u2580\u2588\u2588\u2584 \u2580\u2580${r} ${style.dim("Model:")} ${style.brand(info.model)}`);
4104
- console.log(` ${b}\u2588\u2588\u2588 \u2588\u2588\u258C \u2588\u2588 \u2590\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2584${r} ${style.dim("Role:")} ${style.yellow(info.role)}`);
4408
+ console.log(` ${b}\u2588\u2588\u2588 \u2584\u2588\u2588\u2580 \u2580\u2588\u2588\u2584 \u2580\u2580${r} ${style.dim("User:")} ${userDisplay}`);
4409
+ console.log(` ${b}\u2588\u2588\u2588 \u2588\u2588\u258C \u2588\u2588 \u2590\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2584${r} ${style.dim("Model:")} ${style.brand(info.model)}`);
4105
4410
  console.log(` ${b}\u2588\u2588\u2588 \u2588\u2588\u258C \u2588\u2588 \u2590\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2580${r} ${style.dim("Dir:")} ${style.dim(info.cwd)}`);
4106
4411
  console.log(` ${b}\u2588\u2588\u2588 \u2580\u2588\u2588\u2584 \u2584\u2588\u2588\u2580 \u2584\u2584${r}`);
4107
4412
  console.log(` ${b} \u2588\u2588\u2588 \u2580\u2580\u2588\u2588\u2588\u2588\u2580\u2580 \u2588\u2588\u2588${r}`);
@@ -4119,6 +4424,12 @@ class CliFormatter {
4119
4424
  }).join(" ");
4120
4425
  console.log(` ${style.brand("\u25C6")} ${style.bold("tool:")} ${style.cyan(toolName)} ${argsSummary}`);
4121
4426
  }
4427
+ static formatPatchDiff(filePath, oldSrc, newSrc) {
4428
+ const lines = parsePatch(oldSrc, newSrc);
4429
+ if (lines.length === 0)
4430
+ return;
4431
+ console.log(renderDiff(lines, { filePath }));
4432
+ }
4122
4433
  static formatToolOutput(output, isError = false) {
4123
4434
  const prefix = isError ? style.red(" \u2717 error:") : style.dim(" \u21B3 output:");
4124
4435
  const lines = output.trim().split(`
@@ -4130,6 +4441,62 @@ class CliFormatter {
4130
4441
  console.log(`${prefix}
4131
4442
  ${preview}${more}`);
4132
4443
  }
4444
+ static formatTurnSummary(metrics) {
4445
+ const formatTokens = (n) => {
4446
+ if (n >= 1e6)
4447
+ return `${(n / 1e6).toFixed(1)}M`;
4448
+ if (n >= 1000) {
4449
+ const val = (n / 1000).toFixed(1);
4450
+ return val.endsWith(".0") ? `${val.slice(0, -2)}k` : `${val}k`;
4451
+ }
4452
+ return String(n);
4453
+ };
4454
+ const durationSec = (metrics.durationMs / 1000).toFixed(1);
4455
+ const parts = [`${c.bold}${durationSec}s${c.reset}`];
4456
+ if (metrics.inputTokens !== undefined || metrics.outputTokens !== undefined) {
4457
+ const inStr = metrics.inputTokens !== undefined ? formatTokens(metrics.inputTokens) : "0";
4458
+ const outStr = metrics.outputTokens !== undefined ? formatTokens(metrics.outputTokens) : "0";
4459
+ parts.push(`${style.cyan(`${inStr} in`)} ${style.dim("/")} ${style.cyan(`${outStr} out`)}`);
4460
+ } else if (metrics.totalTokens !== undefined && metrics.totalTokens > 0) {
4461
+ parts.push(`${style.dim(`${formatTokens(metrics.totalTokens)} tokens`)}`);
4462
+ }
4463
+ if (metrics.contextTokens !== undefined && metrics.maxContextTokens !== undefined && metrics.maxContextTokens > 0) {
4464
+ const pct = Math.round(metrics.contextTokens / metrics.maxContextTokens * 100);
4465
+ const colorFn = pct < 50 ? style.green : pct < 80 ? style.yellow : style.red;
4466
+ const ctxLabel = colorFn(`${pct}% context`);
4467
+ parts.push(ctxLabel);
4468
+ }
4469
+ if (metrics.toolCalls && metrics.toolCalls.length > 0) {
4470
+ const count = metrics.toolCalls.length;
4471
+ const uniqueTools = Array.from(new Set(metrics.toolCalls));
4472
+ const toolLabel = count === 1 ? `1 tool (${uniqueTools.join(", ")})` : `${count} tools (${uniqueTools.join(", ")})`;
4473
+ parts.push(`${style.cyan(toolLabel)}`);
4474
+ }
4475
+ if (metrics.filesModified && metrics.filesModified.length > 0) {
4476
+ const count = metrics.filesModified.length;
4477
+ const fileLabel = count === 1 ? `1 file updated` : `${count} files updated`;
4478
+ parts.push(`${style.green(fileLabel)}`);
4479
+ }
4480
+ const dot = `${style.dim(" \xB7 ")}`;
4481
+ console.log(`
4482
+ ${c.brandBold}\u273B${c.reset} ${style.dim("Completed in")} ${parts.join(dot)}`);
4483
+ const sessionParts = [];
4484
+ if (metrics.sessionUptimeMs !== undefined && metrics.sessionUptimeMs >= 1000) {
4485
+ sessionParts.push(`${style.dim("session:")} ${style.bold(formatDuration(metrics.sessionUptimeMs))}`);
4486
+ }
4487
+ if (metrics.subAgents && metrics.subAgents.length > 0) {
4488
+ const agentBadges = metrics.subAgents.map((a) => {
4489
+ const timeStr = formatDuration(a.runningTimeMs);
4490
+ const icon = a.status === "running" ? style.brand("\u25CF") : a.status === "completed" ? style.green("\u2714") : style.red("\u2717");
4491
+ return `${icon} ${style.cyan(a.nickname)} ${style.dim(`(${a.role}, ${timeStr})`)}`;
4492
+ });
4493
+ sessionParts.push(`${style.dim("sub-agents:")} [${agentBadges.join(", ")}]`);
4494
+ }
4495
+ if (sessionParts.length > 0) {
4496
+ console.log(` ${style.dim("\u21B3")} ${sessionParts.join(dot)}`);
4497
+ }
4498
+ console.log();
4499
+ }
4133
4500
  static formatMarkdownLine(chunk) {
4134
4501
  let formatted = chunk.replace(/\*\*(.*?)\*\*/g, `${c.bold}$1${c.reset}`);
4135
4502
  formatted = formatted.replace(/`([^`]+)`/g, `${c.cyan}$1${c.reset}`);
@@ -4158,9 +4525,12 @@ function ensureKeypressInitialized() {
4158
4525
 
4159
4526
  class InteractiveLineEditor {
4160
4527
  promptSymbol;
4528
+ cwd;
4161
4529
  onInterrupt;
4530
+ searchEngine = new FileSearchEngine;
4162
4531
  constructor(options = {}) {
4163
4532
  this.promptSymbol = options.promptSymbol || `${c.brandBold}\u276F${c.reset} `;
4533
+ this.cwd = options.cwd || process.cwd();
4164
4534
  this.onInterrupt = options.onInterrupt;
4165
4535
  }
4166
4536
  async readLine() {
@@ -4178,6 +4548,7 @@ class InteractiveLineEditor {
4178
4548
  let buffer = "";
4179
4549
  let cursor = 0;
4180
4550
  let selectedIndex = 0;
4551
+ let scrollTop = 0;
4181
4552
  let renderedMenuLines = 0;
4182
4553
  let popupDismissed = false;
4183
4554
  if (!globalRawMode && process.stdin.isTTY) {
@@ -4194,6 +4565,47 @@ class InteractiveLineEditor {
4194
4565
  const matches = AVAILABLE_SLASH_COMMANDS.filter((cmd) => cmd.name.startsWith(term));
4195
4566
  return matches.length > 0 ? matches : AVAILABLE_SLASH_COMMANDS;
4196
4567
  };
4568
+ const getActiveFileQuery = () => {
4569
+ if (popupDismissed)
4570
+ return null;
4571
+ const beforeCursor = buffer.slice(0, cursor);
4572
+ const match = beforeCursor.match(/(?:^|\s)@([^\s]*)$/);
4573
+ if (!match)
4574
+ return null;
4575
+ const query = match[1] ?? "";
4576
+ const atIndex = beforeCursor.lastIndexOf("@");
4577
+ return { query, atIndex };
4578
+ };
4579
+ const getMatchingFiles = (fileQuery) => {
4580
+ try {
4581
+ return this.searchEngine.findFiles(this.cwd, {
4582
+ pattern: fileQuery,
4583
+ maxResults: 30
4584
+ });
4585
+ } catch {
4586
+ return [];
4587
+ }
4588
+ };
4589
+ const ensureVisible = (totalItems, visibleRows) => {
4590
+ if (totalItems === 0 || visibleRows === 0) {
4591
+ scrollTop = 0;
4592
+ return;
4593
+ }
4594
+ if (selectedIndex < 0)
4595
+ selectedIndex = 0;
4596
+ if (selectedIndex >= totalItems)
4597
+ selectedIndex = totalItems - 1;
4598
+ if (selectedIndex < scrollTop) {
4599
+ scrollTop = selectedIndex;
4600
+ } else if (selectedIndex >= scrollTop + visibleRows) {
4601
+ scrollTop = selectedIndex + 1 - visibleRows;
4602
+ }
4603
+ if (scrollTop < 0)
4604
+ scrollTop = 0;
4605
+ const maxScroll = Math.max(0, totalItems - visibleRows);
4606
+ if (scrollTop > maxScroll)
4607
+ scrollTop = maxScroll;
4608
+ };
4197
4609
  const clearMenu = () => {
4198
4610
  if (renderedMenuLines > 0) {
4199
4611
  for (let i = 0;i < renderedMenuLines; i++) {
@@ -4207,28 +4619,79 @@ class InteractiveLineEditor {
4207
4619
  const redraw = () => {
4208
4620
  clearMenu();
4209
4621
  process.stdout.write(`\r\x1B[2K${this.promptSymbol}${buffer}`);
4210
- const matches = getMatchingCommands();
4211
- if (buffer.startsWith("/") && !popupDismissed && matches.length > 0) {
4212
- const maxVisible = Math.min(matches.length, 8);
4213
- if (selectedIndex >= matches.length)
4214
- selectedIndex = 0;
4215
- if (selectedIndex < 0)
4216
- selectedIndex = matches.length - 1;
4622
+ const slashMatches = getMatchingCommands();
4623
+ const activeFile = getActiveFileQuery();
4624
+ const fileMatches = activeFile ? getMatchingFiles(activeFile.query) : [];
4625
+ if (buffer.startsWith("/") && !popupDismissed && slashMatches.length > 0) {
4626
+ const BOX_WIDTH = 70;
4627
+ const maxVisible = Math.min(slashMatches.length, 7);
4628
+ ensureVisible(slashMatches.length, maxVisible);
4629
+ const visibleMatches = slashMatches.slice(scrollTop, scrollTop + maxVisible);
4217
4630
  const menuLines = [];
4218
- menuLines.push(` ${style.dim("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510")}`);
4219
- for (let i = 0;i < maxVisible; i++) {
4220
- const cmd = matches[i];
4221
- const isSelected = i === selectedIndex;
4631
+ menuLines.push(` ${style.dim("\u250C" + "\u2500".repeat(BOX_WIDTH) + "\u2510")}`);
4632
+ for (let i = 0;i < visibleMatches.length; i++) {
4633
+ const cmd = visibleMatches[i];
4634
+ const actualIdx = scrollTop + i;
4635
+ const isSelected = actualIdx === selectedIndex;
4222
4636
  const marker = isSelected ? style.brand("\u276F") : " ";
4223
- const name = isSelected ? style.brandBold(cmd.name.padEnd(12)) : style.cyan(cmd.name.padEnd(12));
4224
- const desc = isSelected ? style.bold(cmd.description.padEnd(38)) : style.dim(cmd.description.padEnd(38));
4225
- menuLines.push(` ${style.dim("\u2502")} ${marker} ${name} ${desc} ${style.dim("\u2502")}`);
4637
+ const rawName = cmd.name.padEnd(13).slice(0, 13);
4638
+ const rawDesc = cmd.description.length > 50 ? cmd.description.slice(0, 47) + "..." : cmd.description.padEnd(50);
4639
+ const coloredName = isSelected ? style.brandBold(rawName) : style.cyan(rawName);
4640
+ const coloredDesc = isSelected ? style.bold(rawDesc) : style.dim(rawDesc);
4641
+ menuLines.push(` ${style.dim("\u2502")} ${marker} ${coloredName} ${coloredDesc} ${style.dim("\u2502")}`);
4226
4642
  }
4227
- if (matches.length > maxVisible) {
4228
- const moreCount = matches.length - maxVisible;
4229
- menuLines.push(` ${style.dim(`\u2502 ... and ${moreCount} more commands (use arrows to scroll)`).padEnd(68)} ${style.dim("\u2502")}`);
4643
+ let footerMsg = "";
4644
+ const moreAbove = scrollTop;
4645
+ const moreBelow = Math.max(0, slashMatches.length - (scrollTop + visibleMatches.length));
4646
+ if (moreAbove > 0 && moreBelow > 0) {
4647
+ footerMsg = ` ... ${moreAbove} more above, ${moreBelow} more below (\u2191/\u2193 to scroll)`;
4648
+ } else if (moreBelow > 0) {
4649
+ footerMsg = ` ... and ${moreBelow} more commands (use arrows to scroll)`;
4650
+ } else if (moreAbove > 0) {
4651
+ footerMsg = ` ... and ${moreAbove} more commands above (use arrows to scroll)`;
4652
+ } else {
4653
+ footerMsg = ` ${slashMatches.length} commands (\u2191/\u2193 to navigate \u2022 Tab to select)`;
4230
4654
  }
4231
- menuLines.push(` ${style.dim("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518")}`);
4655
+ const footerPadded = footerMsg.padEnd(BOX_WIDTH).slice(0, BOX_WIDTH);
4656
+ menuLines.push(` ${style.dim("\u2502")}${style.dim(footerPadded)}${style.dim("\u2502")}`);
4657
+ menuLines.push(` ${style.dim("\u2514" + "\u2500".repeat(BOX_WIDTH) + "\u2518")}`);
4658
+ for (const line of menuLines) {
4659
+ process.stdout.write(`
4660
+ \x1B[2K${line}`);
4661
+ }
4662
+ renderedMenuLines = menuLines.length;
4663
+ process.stdout.write(`\x1B[${renderedMenuLines}A`);
4664
+ } else if (activeFile && !popupDismissed && fileMatches.length > 0) {
4665
+ const BOX_WIDTH = 70;
4666
+ const maxVisible = Math.min(fileMatches.length, 7);
4667
+ ensureVisible(fileMatches.length, maxVisible);
4668
+ const visibleMatches = fileMatches.slice(scrollTop, scrollTop + maxVisible);
4669
+ const menuLines = [];
4670
+ menuLines.push(` ${style.dim("\u250C\u2500\u2500")} ${style.brandBold("Files")} ${style.dim("\u2500".repeat(Math.max(10, BOX_WIDTH - 9)) + "\u2510")}`);
4671
+ for (let i = 0;i < visibleMatches.length; i++) {
4672
+ const filePath = visibleMatches[i];
4673
+ const actualIdx = scrollTop + i;
4674
+ const isSelected = actualIdx === selectedIndex;
4675
+ const marker = isSelected ? style.brand("\u276F") : " ";
4676
+ const rawPath = filePath.length > 62 ? "..." + filePath.slice(filePath.length - 59) : filePath.padEnd(62);
4677
+ const coloredPath = isSelected ? style.brandBold(rawPath) : style.cyan(rawPath);
4678
+ menuLines.push(` ${style.dim("\u2502")} ${marker} ${coloredPath} ${style.dim("\u2502")}`);
4679
+ }
4680
+ let footerMsg = "";
4681
+ const moreAbove = scrollTop;
4682
+ const moreBelow = Math.max(0, fileMatches.length - (scrollTop + visibleMatches.length));
4683
+ if (moreAbove > 0 && moreBelow > 0) {
4684
+ footerMsg = ` ... ${moreAbove} more above, ${moreBelow} more below (\u2191/\u2193 to scroll)`;
4685
+ } else if (moreBelow > 0) {
4686
+ footerMsg = ` ... and ${moreBelow} more files (use arrows \u2022 Tab to insert)`;
4687
+ } else if (moreAbove > 0) {
4688
+ footerMsg = ` ... and ${moreAbove} more files above (use arrows \u2022 Tab to insert)`;
4689
+ } else {
4690
+ footerMsg = ` ${fileMatches.length} files (\u2191/\u2193 to navigate \u2022 Tab to insert)`;
4691
+ }
4692
+ const footerPadded = footerMsg.padEnd(BOX_WIDTH).slice(0, BOX_WIDTH);
4693
+ menuLines.push(` ${style.dim("\u2502")}${style.dim(footerPadded)}${style.dim("\u2502")}`);
4694
+ menuLines.push(` ${style.dim("\u2514" + "\u2500".repeat(BOX_WIDTH) + "\u2518")}`);
4232
4695
  for (const line of menuLines) {
4233
4696
  process.stdout.write(`
4234
4697
  \x1B[2K${line}`);
@@ -4259,6 +4722,7 @@ class InteractiveLineEditor {
4259
4722
  buffer = "";
4260
4723
  cursor = 0;
4261
4724
  selectedIndex = 0;
4725
+ scrollTop = 0;
4262
4726
  popupDismissed = false;
4263
4727
  redraw();
4264
4728
  return;
@@ -4290,10 +4754,24 @@ class InteractiveLineEditor {
4290
4754
  cleanupAndResolve("/exit");
4291
4755
  return;
4292
4756
  }
4757
+ const slashMatches = getMatchingCommands();
4758
+ const activeFile = getActiveFileQuery();
4759
+ const fileMatches = activeFile ? getMatchingFiles(activeFile.query) : [];
4293
4760
  if (key.name === "return" || key.name === "enter") {
4294
- const matches = getMatchingCommands();
4295
- if (buffer.startsWith("/") && !popupDismissed && matches.length > 0 && selectedIndex >= 0 && selectedIndex < matches.length) {
4296
- const selected = matches[selectedIndex];
4761
+ if (activeFile && !popupDismissed && fileMatches.length > 0) {
4762
+ const chosen = fileMatches[selectedIndex] || fileMatches[0];
4763
+ const beforeAt = buffer.slice(0, activeFile.atIndex);
4764
+ const afterCursor = buffer.slice(cursor);
4765
+ buffer = `${beforeAt}@${chosen} ${afterCursor}`;
4766
+ cursor = beforeAt.length + chosen.length + 2;
4767
+ selectedIndex = 0;
4768
+ scrollTop = 0;
4769
+ popupDismissed = false;
4770
+ redraw();
4771
+ return;
4772
+ }
4773
+ if (buffer.startsWith("/") && !popupDismissed && slashMatches.length > 0 && selectedIndex >= 0 && selectedIndex < slashMatches.length) {
4774
+ const selected = slashMatches[selectedIndex];
4297
4775
  cleanupAndResolve(selected.name);
4298
4776
  return;
4299
4777
  }
@@ -4301,13 +4779,25 @@ class InteractiveLineEditor {
4301
4779
  return;
4302
4780
  }
4303
4781
  if (key.name === "tab") {
4304
- const matches = getMatchingCommands();
4305
- if (matches.length > 0) {
4306
- const match = matches[selectedIndex] || matches[0];
4782
+ if (activeFile && !popupDismissed && fileMatches.length > 0) {
4783
+ const chosen = fileMatches[selectedIndex] || fileMatches[0];
4784
+ const beforeAt = buffer.slice(0, activeFile.atIndex);
4785
+ const afterCursor = buffer.slice(cursor);
4786
+ buffer = `${beforeAt}@${chosen} ${afterCursor}`;
4787
+ cursor = beforeAt.length + chosen.length + 2;
4788
+ selectedIndex = 0;
4789
+ scrollTop = 0;
4790
+ popupDismissed = false;
4791
+ redraw();
4792
+ return;
4793
+ }
4794
+ if (slashMatches.length > 0) {
4795
+ const match = slashMatches[selectedIndex] || slashMatches[0];
4307
4796
  if (match) {
4308
4797
  buffer = match.name + " ";
4309
4798
  cursor = buffer.length;
4310
4799
  selectedIndex = 0;
4800
+ scrollTop = 0;
4311
4801
  popupDismissed = false;
4312
4802
  redraw();
4313
4803
  }
@@ -4315,17 +4805,25 @@ class InteractiveLineEditor {
4315
4805
  return;
4316
4806
  }
4317
4807
  if (key.name === "up") {
4318
- const matches = getMatchingCommands();
4319
- if (matches.length > 0) {
4320
- selectedIndex = (selectedIndex - 1 + matches.length) % matches.length;
4808
+ if (activeFile && fileMatches.length > 0) {
4809
+ selectedIndex = (selectedIndex - 1 + fileMatches.length) % fileMatches.length;
4810
+ redraw();
4811
+ return;
4812
+ }
4813
+ if (slashMatches.length > 0) {
4814
+ selectedIndex = (selectedIndex - 1 + slashMatches.length) % slashMatches.length;
4321
4815
  redraw();
4322
4816
  return;
4323
4817
  }
4324
4818
  }
4325
4819
  if (key.name === "down") {
4326
- const matches = getMatchingCommands();
4327
- if (matches.length > 0) {
4328
- selectedIndex = (selectedIndex + 1) % matches.length;
4820
+ if (activeFile && fileMatches.length > 0) {
4821
+ selectedIndex = (selectedIndex + 1) % fileMatches.length;
4822
+ redraw();
4823
+ return;
4824
+ }
4825
+ if (slashMatches.length > 0) {
4826
+ selectedIndex = (selectedIndex + 1) % slashMatches.length;
4329
4827
  redraw();
4330
4828
  return;
4331
4829
  }
@@ -4349,6 +4847,7 @@ class InteractiveLineEditor {
4349
4847
  buffer = buffer.slice(0, cursor - 1) + buffer.slice(cursor);
4350
4848
  cursor--;
4351
4849
  selectedIndex = 0;
4850
+ scrollTop = 0;
4352
4851
  popupDismissed = false;
4353
4852
  redraw();
4354
4853
  }
@@ -4358,6 +4857,7 @@ class InteractiveLineEditor {
4358
4857
  buffer = buffer.slice(0, cursor) + _str + buffer.slice(cursor);
4359
4858
  cursor += _str.length;
4360
4859
  selectedIndex = 0;
4860
+ scrollTop = 0;
4361
4861
  popupDismissed = false;
4362
4862
  redraw();
4363
4863
  }
@@ -4371,6 +4871,7 @@ class InteractiveLineEditor {
4371
4871
  // src/cli/commands.ts
4372
4872
  var AVAILABLE_SLASH_COMMANDS = [
4373
4873
  { name: "/help", description: "Show command list and help menu" },
4874
+ { name: "/stats", description: "Display session runtime, turn stats & sub-agent status" },
4374
4875
  { name: "/model", description: "Select or switch active AI model" },
4375
4876
  { name: "/models", description: "List available AI models from gateway" },
4376
4877
  { name: "/reasoning", description: "Toggle internal reasoning chain visibility" },
@@ -4397,6 +4898,11 @@ async function handleSlashCommand(input, ctx) {
4397
4898
  case "/help":
4398
4899
  printHelp();
4399
4900
  return true;
4901
+ case "/stats":
4902
+ case "/time":
4903
+ case "/uptime":
4904
+ printSessionStats(ctx);
4905
+ return true;
4400
4906
  case "/model":
4401
4907
  case "/models":
4402
4908
  await handleModelSelection(ctx, args[0]);
@@ -4736,11 +5242,170 @@ function printSessions(ctx) {
4736
5242
  }
4737
5243
  console.log();
4738
5244
  }
5245
+ function printSessionStats(ctx) {
5246
+ const uptimeMs = ctx.repl ? Date.now() - ctx.repl.sessionStartTime : 0;
5247
+ const turns = ctx.repl?.turnCount ?? 0;
5248
+ const history = ctx.session.getHistory();
5249
+ const historyTokens = estimateTotalTokens(history);
5250
+ const maxTokens = 128000;
5251
+ const contextPct = Math.round(historyTokens / maxTokens * 100);
5252
+ const colorFn = contextPct < 50 ? style.green : contextPct < 80 ? style.yellow : style.red;
5253
+ const agents = ctx.spawner?.listAgents() || [];
5254
+ const activeAgents = agents.filter((a) => a.status === "running");
5255
+ console.log();
5256
+ console.log(style.bold(" \u250C\u2500\u2500 Session Runtime & Statistics \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5257
+ console.log(` \u2502 ${style.dim("Uptime:")} ${style.bold(formatDuration(uptimeMs))}`);
5258
+ console.log(` \u2502 ${style.dim("Turns:")} ${turns} completed`);
5259
+ console.log(` \u2502 ${style.dim("History Items:")} ${history.length} items`);
5260
+ console.log(` \u2502 ${style.dim("Context Usage:")} ${colorFn(`${contextPct}%`)} ${style.dim(`(~${historyTokens} tokens / ${maxTokens} max)`)}`);
5261
+ console.log(` \u2502 ${style.dim("Model:")} ${style.brand(ctx.session.model)}`);
5262
+ console.log(` \u2502 ${style.dim("Working Dir:")} ${style.dim(ctx.session.cwd)}`);
5263
+ console.log(" \u2502");
5264
+ if (agents.length === 0) {
5265
+ console.log(` \u2502 ${style.dim("Sub-agents:")} None spawned yet`);
5266
+ } else {
5267
+ console.log(` \u2502 ${style.bold("Sub-agents:")} (${activeAgents.length} active, ${agents.length - activeAgents.length} completed)`);
5268
+ for (const a of agents) {
5269
+ const runtime = formatDuration(Date.now() - a.createdAt);
5270
+ const icon = a.status === "running" ? style.brand("\u25CF") : a.status === "completed" ? style.green("\u2714") : style.red("\u2717");
5271
+ console.log(` \u2502 ${icon} ${style.cyan(a.nickname)} [${style.dim(a.role)}] - ${a.status} (${runtime})`);
5272
+ console.log(` \u2502 ${style.dim(`Task: ${a.taskName}`)}`);
5273
+ }
5274
+ }
5275
+ console.log(style.bold(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
5276
+ console.log();
5277
+ }
5278
+
5279
+ // src/cli/ui/prompt.ts
5280
+ import readline2 from "readline";
5281
+ async function promptChoice(config) {
5282
+ const { message, choices, defaultIndex = 0 } = config;
5283
+ if (!process.stdin.isTTY) {
5284
+ if (!process.stdin.readable) {
5285
+ return choices[defaultIndex]?.value ?? choices[0].value;
5286
+ }
5287
+ return new Promise((resolve13) => {
5288
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
5289
+ const hint = choices.map((c3) => c3.isDefault ? `[${c3.key.toUpperCase()}]` : `[${c3.key}]`).join("/");
5290
+ let resolved = false;
5291
+ const doResolve = (val) => {
5292
+ if (!resolved) {
5293
+ resolved = true;
5294
+ rl.close();
5295
+ resolve13(val);
5296
+ }
5297
+ };
5298
+ rl.question(`${message} ${hint}: `, (answer) => {
5299
+ const trimmed = answer.trim().toLowerCase();
5300
+ const matched = choices.find((c3) => c3.key.toLowerCase() === trimmed);
5301
+ doResolve(matched ? matched.value : choices[defaultIndex].value);
5302
+ });
5303
+ rl.on("close", () => {
5304
+ doResolve(choices[defaultIndex].value);
5305
+ });
5306
+ });
5307
+ }
5308
+ return new Promise((resolve13) => {
5309
+ let selectedIndex = defaultIndex;
5310
+ if (selectedIndex < 0 || selectedIndex >= choices.length)
5311
+ selectedIndex = 0;
5312
+ readline2.emitKeypressEvents(process.stdin);
5313
+ const wasRaw = process.stdin.isRaw;
5314
+ try {
5315
+ process.stdin.setRawMode(true);
5316
+ } catch {}
5317
+ process.stdin.resume();
5318
+ const render = () => {
5319
+ const parts = choices.map((choice, idx) => {
5320
+ const isSelected = idx === selectedIndex;
5321
+ const keyTag = `[${choice.key}]`;
5322
+ if (isSelected) {
5323
+ return `${c.bgBrand}${c.white}${c.bold} \u276F ${keyTag} ${choice.label} ${c.reset}`;
5324
+ }
5325
+ return `${style.dim(` ${keyTag} ${choice.label}`)}`;
5326
+ });
5327
+ process.stdout.write(`\r\x1B[2K ${style.bold(message)}
5328
+ \r\x1B[2K${parts.join(" ")}`);
5329
+ };
5330
+ const cleanup = (confirmedChoice) => {
5331
+ process.stdin.removeListener("keypress", onKeypress);
5332
+ try {
5333
+ process.stdin.setRawMode(wasRaw ?? false);
5334
+ } catch {}
5335
+ process.stdout.write(`\r\x1B[1A\r\x1B[2K ${style.bold(message)} ${style.cyan(`[${confirmedChoice.key}] ${confirmedChoice.label}`)}
5336
+ \r\x1B[2K`);
5337
+ resolve13(confirmedChoice.value);
5338
+ };
5339
+ const onKeypress = (_str, key) => {
5340
+ if (!key)
5341
+ return;
5342
+ if (key.ctrl && key.name === "c") {
5343
+ cleanup(choices.find((c3) => c3.key.toLowerCase() === "n") || choices[defaultIndex]);
5344
+ return;
5345
+ }
5346
+ if (key.name === "escape") {
5347
+ cleanup(choices.find((c3) => c3.key.toLowerCase() === "n") || choices[defaultIndex]);
5348
+ return;
5349
+ }
5350
+ if (key.name === "return" || key.name === "enter") {
5351
+ cleanup(choices[selectedIndex]);
5352
+ return;
5353
+ }
5354
+ if (key.name === "left") {
5355
+ selectedIndex = (selectedIndex - 1 + choices.length) % choices.length;
5356
+ process.stdout.write("\x1B[1A");
5357
+ render();
5358
+ return;
5359
+ }
5360
+ if (key.name === "right" || key.name === "tab") {
5361
+ selectedIndex = (selectedIndex + 1) % choices.length;
5362
+ process.stdout.write("\x1B[1A");
5363
+ render();
5364
+ return;
5365
+ }
5366
+ const char = _str ? _str.toLowerCase() : key.name ? key.name.toLowerCase() : "";
5367
+ if (char) {
5368
+ const directMatch = choices.find((c3) => c3.key.toLowerCase() === char);
5369
+ if (directMatch) {
5370
+ cleanup(directMatch);
5371
+ return;
5372
+ }
5373
+ }
5374
+ };
5375
+ process.stdin.on("keypress", onKeypress);
5376
+ render();
5377
+ });
5378
+ }
5379
+ async function promptToolApproval(params) {
5380
+ const boxWidth = Math.min(process.stdout.columns ?? 80, 70);
5381
+ const border = "\u2500".repeat(Math.max(10, boxWidth - 24));
5382
+ console.log(`
5383
+ ${style.yellow("\u250C\u2500\u2500")} ${style.bold("Approval Required")} ${style.yellow(border)}`);
5384
+ console.log(` ${style.yellow("\u2502")} ${style.dim("Tool:")} ${style.bold(params.toolName)}`);
5385
+ if (params.command) {
5386
+ console.log(` ${style.yellow("\u2502")} ${style.dim("Command:")} ${style.cyan(params.command)}`);
5387
+ }
5388
+ console.log(` ${style.yellow("\u2502")} ${style.dim("Reason:")} ${style.dim(params.description)}`);
5389
+ console.log(` ${style.yellow("\u2514" + "\u2500".repeat(Math.max(10, boxWidth - 4)))}
5390
+ `);
5391
+ const decision = await promptChoice({
5392
+ message: "Allow execution?",
5393
+ choices: [
5394
+ { key: "y", label: "Yes", value: "yes", isDefault: true },
5395
+ { key: "n", label: "No", value: "no" },
5396
+ { key: "a", label: "Always allow this session", value: "always" }
5397
+ ],
5398
+ defaultIndex: 0
5399
+ });
5400
+ return decision;
5401
+ }
4739
5402
 
4740
5403
  // src/cli/ui/markdown.ts
4741
5404
  class MarkdownHighlighter {
4742
5405
  inCodeBlock = false;
4743
5406
  currentLanguage = "";
5407
+ diffBuffer = [];
5408
+ inDiffBlock = false;
4744
5409
  lineBuffer = "";
4745
5410
  static highlight(markdown) {
4746
5411
  const highlighter = new MarkdownHighlighter;
@@ -4774,17 +5439,53 @@ class MarkdownHighlighter {
4774
5439
  if (!this.inCodeBlock) {
4775
5440
  this.inCodeBlock = true;
4776
5441
  this.currentLanguage = fenceMatch[1] || "";
5442
+ if (this.currentLanguage.toLowerCase() === "diff") {
5443
+ this.inDiffBlock = true;
5444
+ this.diffBuffer = [];
5445
+ return "";
5446
+ }
4777
5447
  const langBadge = this.currentLanguage ? ` ${style.brandBold(this.currentLanguage.toUpperCase())} ` : "";
4778
5448
  return `
4779
5449
  ${style.dim("\u250C\u2500\u2500")}${langBadge}${style.dim("\u2500".repeat(Math.max(10, 60 - (this.currentLanguage.length + 6))))}`;
4780
5450
  } else {
4781
5451
  this.inCodeBlock = false;
4782
5452
  this.currentLanguage = "";
5453
+ if (this.inDiffBlock) {
5454
+ this.inDiffBlock = false;
5455
+ const raw = this.diffBuffer.join(`
5456
+ `);
5457
+ this.diffBuffer = [];
5458
+ const oldLines = [];
5459
+ const newLines = [];
5460
+ for (const l of raw.split(`
5461
+ `)) {
5462
+ if (l.startsWith("-")) {
5463
+ oldLines.push(l.slice(1));
5464
+ newLines.push("");
5465
+ } else if (l.startsWith("+")) {
5466
+ oldLines.push("");
5467
+ newLines.push(l.slice(1));
5468
+ } else {
5469
+ oldLines.push(l.startsWith(" ") ? l.slice(1) : l);
5470
+ newLines.push(l.startsWith(" ") ? l.slice(1) : l);
5471
+ }
5472
+ }
5473
+ const diffLines = parsePatch(oldLines.join(`
5474
+ `), newLines.join(`
5475
+ `), 3);
5476
+ return `
5477
+ ` + renderDiff(diffLines) + `
5478
+ `;
5479
+ }
4783
5480
  return ` ${style.dim("\u2514" + "\u2500".repeat(60))}
4784
5481
  `;
4785
5482
  }
4786
5483
  }
4787
5484
  if (this.inCodeBlock) {
5485
+ if (this.inDiffBlock) {
5486
+ this.diffBuffer.push(line);
5487
+ return "";
5488
+ }
4788
5489
  const highlightedCode = this.highlightCode(line, this.currentLanguage);
4789
5490
  return ` ${style.dim("\u2502")} ${highlightedCode}`;
4790
5491
  }
@@ -4941,6 +5642,8 @@ ${style.bold(c.brightCyan + text.replace(/^##\s+/, "\u25A0 "))}${c.reset}`;
4941
5642
  // src/cli/repl.ts
4942
5643
  class CliRepl {
4943
5644
  showReasoning = false;
5645
+ sessionStartTime = Date.now();
5646
+ turnCount = 0;
4944
5647
  session;
4945
5648
  spawner;
4946
5649
  mcpManager;
@@ -4956,6 +5659,11 @@ class CliRepl {
4956
5659
  isClosed = false;
4957
5660
  highlighter = new MarkdownHighlighter;
4958
5661
  turnDoneResolver;
5662
+ turnStartTime = 0;
5663
+ turnToolCalls = [];
5664
+ turnFilesModified = new Set;
5665
+ turnCharsOut = 0;
5666
+ activeToolArgs = {};
4959
5667
  constructor(options) {
4960
5668
  this.session = options.session;
4961
5669
  this.spawner = options.spawner;
@@ -4974,9 +5682,13 @@ class CliRepl {
4974
5682
  switch (msg.type) {
4975
5683
  case "TurnStarted":
4976
5684
  this.isProcessing = true;
5685
+ this.turnCount++;
4977
5686
  this.currentTurnHasOutput = false;
4978
5687
  this.reasoningStarted = false;
4979
- this.highlighter = new MarkdownHighlighter;
5688
+ this.turnStartTime = performance.now();
5689
+ this.turnToolCalls = [];
5690
+ this.turnFilesModified.clear();
5691
+ this.turnCharsOut = 0;
4980
5692
  this.spinner.start("Thinking...");
4981
5693
  break;
4982
5694
  case "ReasoningDelta":
@@ -4985,7 +5697,7 @@ class CliRepl {
4985
5697
  if (!this.reasoningStarted) {
4986
5698
  this.spinner.stop();
4987
5699
  console.log(style.dim(`
4988
- \u250C\u2500\u2500 \uD83D\uDCAD Thinking \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
5700
+ \u250C\u2500\u2500 Thinking \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
4989
5701
  this.reasoningStarted = true;
4990
5702
  }
4991
5703
  process.stdout.write(style.dim(msg.delta));
@@ -5004,6 +5716,7 @@ class CliRepl {
5004
5716
  this.spinner.stop();
5005
5717
  this.currentTurnHasOutput = true;
5006
5718
  }
5719
+ this.turnCharsOut += msg.delta.length;
5007
5720
  if (this.currentTurnHasOutput) {
5008
5721
  const formatted = this.highlighter.feed(msg.delta);
5009
5722
  if (formatted) {
@@ -5019,13 +5732,18 @@ class CliRepl {
5019
5732
  this.reasoningStarted = false;
5020
5733
  }
5021
5734
  this.spinner.stop();
5735
+ this.turnToolCalls.push(msg.toolName);
5736
+ this.activeToolArgs = msg.arguments || {};
5737
+ if ((msg.toolName === "apply_patch" || msg.toolName === "write_file") && msg.arguments && typeof msg.arguments.path === "string") {
5738
+ this.turnFilesModified.add(msg.arguments.path);
5739
+ }
5022
5740
  console.log();
5023
- formatToolCard(msg.toolName, msg.arguments);
5024
- this.spinner.start(`Executing ${msg.toolName}...`);
5741
+ formatTaskStepStart(this.turnToolCalls.length, msg.toolName, msg.arguments);
5742
+ this.spinner.start(`Executing [${this.turnToolCalls.length}] ${msg.toolName}...`);
5025
5743
  break;
5026
5744
  case "ToolCallFinished":
5027
5745
  this.spinner.stop();
5028
- formatToolCard(msg.toolName, {}, msg.output, msg.isError);
5746
+ formatTaskStepFinish(this.turnToolCalls.length, msg.toolName, this.activeToolArgs, msg.output, msg.isError);
5029
5747
  break;
5030
5748
  case "InteractiveApprovalRequired":
5031
5749
  case "ApprovalRequired":
@@ -5045,11 +5763,26 @@ class CliRepl {
5045
5763
  process.stdout.write(flushed);
5046
5764
  }
5047
5765
  this.isProcessing = false;
5048
- console.log();
5049
- if (msg.totalTokens) {
5050
- console.log(style.dim(`[Turn completed | ${msg.totalTokens} tokens]`));
5051
- }
5052
- console.log();
5766
+ const durationMs = this.turnStartTime > 0 ? performance.now() - this.turnStartTime : 0;
5767
+ const sessionUptimeMs = Date.now() - this.sessionStartTime;
5768
+ const subAgents = this.spawner?.listAgents().map((a) => ({
5769
+ nickname: a.nickname,
5770
+ role: a.role,
5771
+ status: a.status,
5772
+ runningTimeMs: Date.now() - a.createdAt
5773
+ }));
5774
+ formatTurnSummary({
5775
+ durationMs,
5776
+ inputTokens: msg.inputTokens,
5777
+ outputTokens: msg.outputTokens || (this.turnCharsOut > 0 ? Math.round(this.turnCharsOut / 3.8) : undefined),
5778
+ totalTokens: msg.totalTokens,
5779
+ contextTokens: msg.contextTokens,
5780
+ maxContextTokens: msg.maxContextTokens,
5781
+ sessionUptimeMs,
5782
+ subAgents,
5783
+ toolCalls: this.turnToolCalls,
5784
+ filesModified: Array.from(this.turnFilesModified)
5785
+ });
5053
5786
  if (this.turnDoneResolver) {
5054
5787
  const resolve13 = this.turnDoneResolver;
5055
5788
  this.turnDoneResolver = undefined;
@@ -5078,35 +5811,34 @@ class CliRepl {
5078
5811
  });
5079
5812
  }
5080
5813
  async handleInteractiveApproval(msg) {
5081
- console.log();
5082
- console.log(style.yellow(`[!] Approval Required for action:`));
5083
- console.log(` Tool: ${style.bold(msg.toolName)}`);
5084
- if (msg.command) {
5085
- console.log(` Command: ${style.cyan(msg.command)}`);
5086
- }
5087
- console.log(` Reason: ${style.dim(msg.description)}`);
5088
5814
  try {
5089
- const editor = new InteractiveLineEditor({ promptSymbol: style.bold(`
5090
- Allow execution? [y/N]: `) });
5091
- const answer = (await editor.readLine()).trim().toLowerCase();
5092
- const approved = answer === "y" || answer === "yes";
5093
- this.session.resolveApproval(msg.approvalId, approved);
5094
- if (approved) {
5815
+ const decision = await promptToolApproval(msg);
5816
+ if (decision === "always") {
5817
+ this.session.execPolicy.addRule(/.*/, "allow", "User allowed all actions for this session");
5818
+ this.session.resolveApproval(msg.approvalId, true);
5819
+ this.spinner.start(`Executing approved action (auto-approved for session)...`);
5820
+ } else if (decision === "yes") {
5821
+ this.session.resolveApproval(msg.approvalId, true);
5095
5822
  this.spinner.start(`Executing approved action...`);
5096
5823
  } else {
5097
- console.log(style.dim("Action rejected by user."));
5824
+ this.session.resolveApproval(msg.approvalId, false);
5825
+ console.log(style.dim(" Action rejected by user."));
5098
5826
  }
5099
5827
  } catch {
5100
5828
  this.session.resolveApproval(msg.approvalId, false);
5101
5829
  }
5102
5830
  }
5103
5831
  async start() {
5832
+ const creds = new CredentialsStore().load();
5833
+ const accountUser = creds?.user?.username || creds?.user?.email || (creds?.accessToken ? "Authenticated" : undefined);
5104
5834
  renderGroupyBanner({
5835
+ user: accountUser,
5105
5836
  role: this.role,
5106
5837
  model: this.session.model,
5107
5838
  cwd: this.session.cwd
5108
5839
  });
5109
5840
  const editor = new InteractiveLineEditor({
5841
+ cwd: this.session.cwd,
5110
5842
  onInterrupt: () => {
5111
5843
  if (this.isProcessing) {
5112
5844
  const activeTurn = this.session.getActiveTurn();
@@ -5178,6 +5910,8 @@ async function main() {
5178
5910
  let model = process.env.GROUPY_MODEL || process.env.OPENAI_MODEL || "groupy";
5179
5911
  let baseUrl = process.env.GROUPY_BASE_URL || process.env.OPENAI_BASE_URL || savedCreds?.baseUrl;
5180
5912
  let apiKey = process.env.GROUPY_API_KEY || process.env.OPENAI_API_KEY || savedCreds?.accessToken;
5913
+ let explicitBaseUrl;
5914
+ let explicitApiKey;
5181
5915
  let cwd = process.cwd();
5182
5916
  let role = "default";
5183
5917
  let mcpConfigFile;
@@ -5202,7 +5936,7 @@ async function main() {
5202
5936
  printWhoami2(credStore);
5203
5937
  process.exit(0);
5204
5938
  } else if (arg === "models" || arg === "--models") {
5205
- await printAvailableModels(baseUrl || "http://localhost:8090/v1", apiKey);
5939
+ await printAvailableModels(baseUrl || "https://api.groupy-hub.store/v1", apiKey);
5206
5940
  process.exit(0);
5207
5941
  } else if (arg === "list" || arg === "sessions" || arg === "--list") {
5208
5942
  printSessionsList(storageManager);
@@ -5221,15 +5955,20 @@ async function main() {
5221
5955
  } else if (arg === "--model" || arg === "-m") {
5222
5956
  model = args[++i] || model;
5223
5957
  } else if (arg === "--base-url" || arg === "-u") {
5224
- baseUrl = args[++i] || baseUrl;
5958
+ explicitBaseUrl = args[++i];
5959
+ baseUrl = explicitBaseUrl || baseUrl;
5225
5960
  } else if (arg === "--api-key" || arg === "-k") {
5226
- apiKey = args[++i] || apiKey;
5961
+ explicitApiKey = args[++i];
5962
+ apiKey = explicitApiKey || apiKey;
5227
5963
  } else if (arg === "--cwd" || arg === "-C") {
5228
5964
  cwd = resolve13(args[++i] || cwd);
5229
5965
  } else if (arg === "--role" || arg === "-r") {
5230
5966
  role = args[++i] || role;
5231
5967
  } else if (arg === "--mcp") {
5232
5968
  mcpConfigFile = args[++i];
5969
+ } else if (arg === "--version" || arg === "-v" || arg === "version") {
5970
+ console.log(`pikaa v0.2.4`);
5971
+ process.exit(0);
5233
5972
  } else if (arg === "--help" || arg === "-h") {
5234
5973
  printCliHelp();
5235
5974
  process.exit(0);
@@ -5238,8 +5977,8 @@ async function main() {
5238
5977
  }
5239
5978
  }
5240
5979
  const modelClient = new ModelClient({
5241
- baseUrl,
5242
- apiKey,
5980
+ baseUrl: explicitBaseUrl,
5981
+ apiKey: explicitApiKey,
5243
5982
  defaultModel: model
5244
5983
  });
5245
5984
  const tools = createDefaultTools({ skillsLoader, memoryStore, worktreeManager });
@@ -5330,7 +6069,7 @@ Execution failed: ${err instanceof Error ? err.message : String(err)}`));
5330
6069
  async function handleLogin(authClient, backendUrl) {
5331
6070
  const targetBackend = backendUrl || process.env.GROUPY_BACKEND_URL || "https://api.groupy-hub.store";
5332
6071
  console.log(style.brand(`
5333
- \uD83D\uDD10 Logging into Backend: ${targetBackend}`));
6072
+ Logging into Backend: ${targetBackend}`));
5334
6073
  try {
5335
6074
  const { authUrl, waitForToken } = await authClient.startOAuthFlow({
5336
6075
  backendUrl: targetBackend
@@ -5495,6 +6234,7 @@ ${style.bold("OPTIONS:")}
5495
6234
  -C, --cwd <path> Working directory for agent operations (default: current dir)
5496
6235
  -r, --role <role> Initial agent role (default, reviewer, researcher, tester, planner)
5497
6236
  --mcp <path> Path to MCP server configuration JSON file
6237
+ -v, --version Show CLI version
5498
6238
  -h, --help Show this help message
5499
6239
 
5500
6240
  ${style.bold("EXAMPLES:")}