@pikaa-ai/pikaa 0.2.2 → 0.2.3

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;
@@ -523,6 +537,8 @@ async function runTurn(session, turnContext, input) {
523
537
  skillsPrompt
524
538
  });
525
539
  let iteration = 0;
540
+ let accumulatedInputTokens = 0;
541
+ let accumulatedOutputTokens = 0;
526
542
  const clientSession = session.modelClient.newSession();
527
543
  try {
528
544
  while (iteration < turnContext.maxIterations) {
@@ -532,6 +548,8 @@ async function runTurn(session, turnContext, input) {
532
548
  iteration++;
533
549
  let currentAgentText = "";
534
550
  const toolCallRequests = [];
551
+ let iterInputTokens = Math.ceil((effectiveSystemPrompt.length + JSON.stringify(session.getHistory()).length) / 4);
552
+ let iterOutputTokens = 0;
535
553
  const stream = clientSession.stream({
536
554
  model: turnContext.model,
537
555
  systemPrompt: effectiveSystemPrompt,
@@ -558,10 +576,20 @@ async function runTurn(session, turnContext, input) {
558
576
  });
559
577
  } else if (chunk.type === "tool_call") {
560
578
  toolCallRequests.push(chunk);
579
+ } else if (chunk.type === "done") {
580
+ if (chunk.inputTokens !== undefined)
581
+ iterInputTokens = chunk.inputTokens;
582
+ if (chunk.outputTokens !== undefined)
583
+ iterOutputTokens = chunk.outputTokens;
561
584
  } else if (chunk.type === "error") {
562
585
  throw chunk.error;
563
586
  }
564
587
  }
588
+ if (iterOutputTokens === 0) {
589
+ iterOutputTokens = Math.ceil((currentAgentText.length + JSON.stringify(toolCallRequests).length) / 4);
590
+ }
591
+ accumulatedInputTokens += iterInputTokens;
592
+ accumulatedOutputTokens += iterOutputTokens;
565
593
  if (currentAgentText.trim()) {
566
594
  const agentItem = {
567
595
  id: `msg_agent_${Date.now()}`,
@@ -602,6 +630,7 @@ async function runTurn(session, turnContext, input) {
602
630
  cwd: turnContext.environment.cwd,
603
631
  turnId,
604
632
  signal,
633
+ execPolicy: session.execPolicy,
605
634
  requestApproval: async (description, command) => {
606
635
  const approvalId = `appr_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
607
636
  return session.requestApproval({
@@ -639,9 +668,16 @@ async function runTurn(session, turnContext, input) {
639
668
  }
640
669
  break;
641
670
  }
671
+ const totalContextTokens = estimateTotalTokens(session.getHistory()) + Math.ceil(effectiveSystemPrompt.length / 4);
672
+ const maxContextTokens = 128000;
642
673
  session.emitEvent({
643
674
  type: "TurnCompleted",
644
- turnId
675
+ turnId,
676
+ inputTokens: accumulatedInputTokens,
677
+ outputTokens: accumulatedOutputTokens,
678
+ totalTokens: accumulatedInputTokens + accumulatedOutputTokens,
679
+ contextTokens: totalContextTokens,
680
+ maxContextTokens
645
681
  });
646
682
  } catch (error) {
647
683
  const isAborted = error instanceof TurnAbortedError || signal.aborted;
@@ -725,6 +761,40 @@ async function submissionLoop(session, queue) {
725
761
  }
726
762
  }
727
763
 
764
+ // src/security/exec-policy.ts
765
+ class ExecPolicy {
766
+ rules = [];
767
+ constructor() {
768
+ this.initDefaultRules();
769
+ }
770
+ initDefaultRules() {
771
+ this.addRule(/^(git\s+(status|log|diff|branch|show|rev-parse))/i, "allow", "Safe git query");
772
+ this.addRule(/^(ls|dir|cat|grep|rg|find|pwd|echo|head|tail|wc|which|where)\b/i, "allow", "Safe read-only shell command");
773
+ this.addRule(/^(bun\s+(test|--version|-v)|npm\s+(test|--version|-v)|node\s+-v)\b/i, "allow", "Testing & runtime check");
774
+ this.addRule(/^(rm|del|rmdir|format|mkfs)\b/i, "prompt", "Destructive file removal");
775
+ this.addRule(/^(git\s+(push|reset\s+--hard|clean\s+-fd|rebase))\b/i, "prompt", "Destructive git operation");
776
+ this.addRule(/^(curl|wget|fetch|ssh|scp|ftp)\b/i, "prompt", "Network / remote transfer");
777
+ }
778
+ addRule(pattern, decision, description) {
779
+ this.rules.unshift({ pattern, decision, description });
780
+ }
781
+ evaluate(command) {
782
+ const trimmed = command.trim();
783
+ for (const rule of this.rules) {
784
+ if (rule.pattern.test(trimmed)) {
785
+ return {
786
+ decision: rule.decision,
787
+ reason: rule.description
788
+ };
789
+ }
790
+ }
791
+ return {
792
+ decision: "prompt",
793
+ reason: "Command is not in the automatic allowlist"
794
+ };
795
+ }
796
+ }
797
+
728
798
  // src/session/session.ts
729
799
  class Session {
730
800
  threadId;
@@ -735,6 +805,7 @@ class Session {
735
805
  tools;
736
806
  skillsLoader;
737
807
  memoryStore;
808
+ execPolicy;
738
809
  history = [];
739
810
  activeTurn = null;
740
811
  status = "idle";
@@ -752,6 +823,7 @@ class Session {
752
823
  this.tools = options.tools || new ToolRouter;
753
824
  this.skillsLoader = options.skillsLoader;
754
825
  this.memoryStore = options.memoryStore;
826
+ this.execPolicy = options.execPolicy || new ExecPolicy;
755
827
  this.history = options.initialHistory ? [...options.initialHistory] : [];
756
828
  if (options.onEvent) {
757
829
  this.eventListeners.push(options.onEvent);
@@ -1001,40 +1073,6 @@ var applyPatchTool = {
1001
1073
  }
1002
1074
  }
1003
1075
  };
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
1076
  // src/tools/handlers/shell.ts
1039
1077
  function createShellTool(policy = new ExecPolicy) {
1040
1078
  return {
@@ -1059,7 +1097,8 @@ function createShellTool(policy = new ExecPolicy) {
1059
1097
  if (!command) {
1060
1098
  return { output: "Error: 'command' argument cannot be empty", isError: true };
1061
1099
  }
1062
- const policyDecision = policy.evaluate(command);
1100
+ const activePolicy = ctx.execPolicy || policy;
1101
+ const policyDecision = activePolicy.evaluate(command);
1063
1102
  if (policyDecision.decision === "deny") {
1064
1103
  return {
1065
1104
  output: `Error: Command execution denied by policy: ${policyDecision.reason}`,
@@ -3904,7 +3943,7 @@ class AuthClient {
3904
3943
  </head>
3905
3944
  <body>
3906
3945
  <div class="box">
3907
- <h1>\u26A1 Authentication Successful!</h1>
3946
+ <h1>Authentication Successful!</h1>
3908
3947
  <p>You have successfully logged in to Groupy CLI. You can close this window and return to your terminal.</p>
3909
3948
  </div>
3910
3949
  </body>
@@ -4015,12 +4054,29 @@ var style = {
4015
4054
  };
4016
4055
 
4017
4056
  // src/cli/ui/spinner.ts
4057
+ function formatDuration(ms) {
4058
+ const totalSec = Math.floor(ms / 1000);
4059
+ if (totalSec < 60) {
4060
+ const sec = (ms / 1000).toFixed(1);
4061
+ return `${sec}s`;
4062
+ }
4063
+ const minutes = Math.floor(totalSec / 60);
4064
+ const seconds = totalSec % 60;
4065
+ if (minutes < 60) {
4066
+ return `${minutes}m ${seconds}s`;
4067
+ }
4068
+ const hours = Math.floor(minutes / 60);
4069
+ const remMinutes = minutes % 60;
4070
+ return `${hours}h ${remMinutes}m ${seconds}s`;
4071
+ }
4072
+
4018
4073
  class LiveSpinner {
4019
4074
  frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
4020
4075
  frameIndex = 0;
4021
4076
  timer = null;
4022
4077
  currentMessage = "";
4023
4078
  isSpinning = false;
4079
+ startTime = 0;
4024
4080
  start(message = "Thinking...") {
4025
4081
  if (this.isSpinning) {
4026
4082
  this.update(message);
@@ -4028,6 +4084,7 @@ class LiveSpinner {
4028
4084
  }
4029
4085
  this.isSpinning = true;
4030
4086
  this.currentMessage = message;
4087
+ this.startTime = performance.now();
4031
4088
  this.frameIndex = 0;
4032
4089
  this.render();
4033
4090
  this.timer = setInterval(() => {
@@ -4046,7 +4103,10 @@ class LiveSpinner {
4046
4103
  }
4047
4104
  render() {
4048
4105
  const frame = this.frames[this.frameIndex];
4049
- const output = `\r\x1B[K${c.cyan}${frame}${c.reset} ${c.dim}${this.currentMessage}${c.reset}`;
4106
+ const elapsedMs = performance.now() - this.startTime;
4107
+ const elapsedStr = formatDuration(elapsedMs);
4108
+ const timeBadge = style.dim(`(${elapsedStr})`);
4109
+ const output = `\r\x1B[K ${c.cyan}${frame}${c.reset} ${c.dim}${this.currentMessage}${c.reset} ${timeBadge}`;
4050
4110
  process.stdout.write(output);
4051
4111
  }
4052
4112
  stop(finalMessage, success = true) {
@@ -4078,30 +4138,271 @@ class LiveSpinner {
4078
4138
  }
4079
4139
  }
4080
4140
 
4141
+ // src/cli/ui/diff.ts
4142
+ var ESC2 = "\x1B[";
4143
+ var R = `${ESC2}0m`;
4144
+ var fg = {
4145
+ lineNum: `${ESC2}38;2;120;120;130m`,
4146
+ gutterDel: `${ESC2}38;2;210;90;90m`,
4147
+ gutterAdd: `${ESC2}38;2;80;200;120m`,
4148
+ textDel: `${ESC2}38;2;235;150;150m`,
4149
+ textAdd: `${ESC2}38;2;140;230;160m`,
4150
+ ctx: `${ESC2}38;2;180;180;195m`
4151
+ };
4152
+ var bg = {
4153
+ del: `${ESC2}48;2;60;20;25m`,
4154
+ add: `${ESC2}48;2;18;55;28m`,
4155
+ wordDel: `${ESC2}48;2;135;40;45m`,
4156
+ wordAdd: `${ESC2}48;2;30;115;50m`
4157
+ };
4158
+ function tokenize(line) {
4159
+ return line.match(/\w+|\s+|[^\w\s]+/g) ?? [];
4160
+ }
4161
+ function lcsTokenDiff(oldTokens, newTokens) {
4162
+ const m = oldTokens.length;
4163
+ const n = newTokens.length;
4164
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
4165
+ for (let i2 = 1;i2 <= m; i2++) {
4166
+ for (let j2 = 1;j2 <= n; j2++) {
4167
+ 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]);
4168
+ }
4169
+ }
4170
+ const ops = [];
4171
+ let i = m, j = n;
4172
+ while (i > 0 || j > 0) {
4173
+ if (i > 0 && j > 0 && oldTokens[i - 1] === newTokens[j - 1]) {
4174
+ ops.push({ same: oldTokens[i - 1] });
4175
+ i--;
4176
+ j--;
4177
+ } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
4178
+ ops.push({ add: newTokens[j - 1] });
4179
+ j--;
4180
+ } else {
4181
+ ops.push({ del: oldTokens[i - 1] });
4182
+ i--;
4183
+ }
4184
+ }
4185
+ return ops.reverse();
4186
+ }
4187
+ function visibleLen(s) {
4188
+ return s.replace(/\x1b\[[0-9;]*m/g, "").length;
4189
+ }
4190
+ function padToWidth(rendered, bgColor, termWidth) {
4191
+ const pad = Math.max(0, termWidth - visibleLen(rendered));
4192
+ return rendered + bgColor + " ".repeat(pad) + R;
4193
+ }
4194
+ function gutterWidth(maxLine) {
4195
+ return Math.max(1, String(maxLine).length);
4196
+ }
4197
+ function renderCtxRow(text, oldLine, newLine, numWidth, termWidth) {
4198
+ const oldNum = String(oldLine).padStart(numWidth);
4199
+ const newNum = String(newLine).padStart(numWidth);
4200
+ const gutter = `${fg.lineNum}${oldNum}${R} ${fg.lineNum}${newNum}${R} `;
4201
+ const body = `${fg.ctx}${text}${R}`;
4202
+ const raw = gutter + body;
4203
+ const pad = Math.max(0, termWidth - visibleLen(raw));
4204
+ return raw + " ".repeat(pad);
4205
+ }
4206
+ function renderDelRow(text, oldLine, numWidth, pairText, termWidth) {
4207
+ const num = String(oldLine).padStart(numWidth);
4208
+ const gutter = `${bg.del}${fg.lineNum}${num}${R}${bg.del}${fg.gutterDel} - ${R}`;
4209
+ let body;
4210
+ if (pairText !== undefined) {
4211
+ const ops = lcsTokenDiff(tokenize(text), tokenize(pairText));
4212
+ body = ops.map((op) => {
4213
+ if (op.same)
4214
+ return `${bg.del}${fg.textDel}${op.same}`;
4215
+ if (op.del)
4216
+ return `${bg.wordDel}${fg.textDel}${op.del}`;
4217
+ return "";
4218
+ }).join("") + R;
4219
+ } else {
4220
+ body = `${bg.del}${fg.textDel}${text}${R}`;
4221
+ }
4222
+ return padToWidth(gutter + body, bg.del, termWidth);
4223
+ }
4224
+ function renderAddRow(text, newLine, numWidth, pairText, termWidth) {
4225
+ const num = String(newLine).padStart(numWidth);
4226
+ const gutter = `${bg.add}${fg.lineNum}${num}${R}${bg.add}${fg.gutterAdd} + ${R}`;
4227
+ let body;
4228
+ if (pairText !== undefined) {
4229
+ const ops = lcsTokenDiff(tokenize(pairText), tokenize(text));
4230
+ body = ops.map((op) => {
4231
+ if (op.same)
4232
+ return `${bg.add}${fg.textAdd}${op.same}`;
4233
+ if (op.add)
4234
+ return `${bg.wordAdd}${fg.textAdd}${op.add}`;
4235
+ return "";
4236
+ }).join("") + R;
4237
+ } else {
4238
+ body = `${bg.add}${fg.textAdd}${text}${R}`;
4239
+ }
4240
+ return padToWidth(gutter + body, bg.add, termWidth);
4241
+ }
4242
+ function renderDiff(lines, opts = {}) {
4243
+ const termWidth = opts.termWidth ?? process.stdout.columns ?? 80;
4244
+ const maxLineNum = lines.reduce((m, l) => Math.max(m, l.oldLine ?? 0, l.newLine ?? 0), 0);
4245
+ const numWidth = gutterWidth(maxLineNum);
4246
+ const out = [];
4247
+ if (opts.filePath) {
4248
+ const headerText = ` ${opts.filePath}`;
4249
+ const padded = headerText.padEnd(termWidth);
4250
+ out.push(`${ESC2}48;2;30;30;40m${ESC2}38;2;200;180;255m${ESC2}1m${padded}${R}`);
4251
+ }
4252
+ let i = 0;
4253
+ while (i < lines.length) {
4254
+ const line = lines[i];
4255
+ if (line.kind === "ctx") {
4256
+ if (line.oldLine === undefined) {
4257
+ const sep = " \u22EF".padEnd(termWidth);
4258
+ out.push(`${ESC2}38;2;100;100;120m${sep}${R}`);
4259
+ } else {
4260
+ out.push(renderCtxRow(line.text, line.oldLine, line.newLine, numWidth, termWidth));
4261
+ }
4262
+ i++;
4263
+ continue;
4264
+ }
4265
+ const dels = [];
4266
+ const adds = [];
4267
+ while (i < lines.length && lines[i].kind === "del")
4268
+ dels.push(lines[i++]);
4269
+ while (i < lines.length && lines[i].kind === "add")
4270
+ adds.push(lines[i++]);
4271
+ const pairCount = Math.min(dels.length, adds.length);
4272
+ for (let p = 0;p < pairCount; p++) {
4273
+ out.push(renderDelRow(dels[p].text, dels[p].oldLine, numWidth, adds[p].text, termWidth));
4274
+ out.push(renderAddRow(adds[p].text, adds[p].newLine, numWidth, dels[p].text, termWidth));
4275
+ }
4276
+ for (let p = pairCount;p < dels.length; p++) {
4277
+ out.push(renderDelRow(dels[p].text, dels[p].oldLine, numWidth, undefined, termWidth));
4278
+ }
4279
+ for (let p = pairCount;p < adds.length; p++) {
4280
+ out.push(renderAddRow(adds[p].text, adds[p].newLine, numWidth, undefined, termWidth));
4281
+ }
4282
+ }
4283
+ return out.join(`
4284
+ `);
4285
+ }
4286
+ function parsePatch(oldSrc, newSrc, contextLines = 3) {
4287
+ const oldLines = oldSrc.split(`
4288
+ `);
4289
+ const newLines = newSrc.split(`
4290
+ `);
4291
+ const m = oldLines.length;
4292
+ const n = newLines.length;
4293
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
4294
+ for (let i = 1;i <= m; i++) {
4295
+ for (let j = 1;j <= n; j++) {
4296
+ 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]);
4297
+ }
4298
+ }
4299
+ const ops = [];
4300
+ let oi = m, ni = n;
4301
+ while (oi > 0 || ni > 0) {
4302
+ if (oi > 0 && ni > 0 && oldLines[oi - 1] === newLines[ni - 1]) {
4303
+ ops.push({ kind: "same", oi: oi - 1, ni: ni - 1 });
4304
+ oi--;
4305
+ ni--;
4306
+ } else if (ni > 0 && (oi === 0 || dp[oi][ni - 1] >= dp[oi - 1][ni])) {
4307
+ ops.push({ kind: "add", ni: ni - 1 });
4308
+ ni--;
4309
+ } else {
4310
+ ops.push({ kind: "del", oi: oi - 1 });
4311
+ oi--;
4312
+ }
4313
+ }
4314
+ ops.reverse();
4315
+ const changed = new Set;
4316
+ ops.forEach((op, idx) => {
4317
+ if (op.kind !== "same")
4318
+ changed.add(idx);
4319
+ });
4320
+ const visible = new Set;
4321
+ changed.forEach((idx) => {
4322
+ for (let k = Math.max(0, idx - contextLines);k <= Math.min(ops.length - 1, idx + contextLines); k++) {
4323
+ visible.add(k);
4324
+ }
4325
+ });
4326
+ const result = [];
4327
+ let prevVisible = -2;
4328
+ for (let idx = 0;idx < ops.length; idx++) {
4329
+ if (!visible.has(idx))
4330
+ continue;
4331
+ if (prevVisible !== -2 && idx > prevVisible + 1) {
4332
+ result.push({ kind: "ctx", text: "\u22EF", oldLine: undefined, newLine: undefined });
4333
+ }
4334
+ prevVisible = idx;
4335
+ const op = ops[idx];
4336
+ if (op.kind === "same") {
4337
+ result.push({ kind: "ctx", oldLine: op.oi + 1, newLine: op.ni + 1, text: oldLines[op.oi] });
4338
+ } else if (op.kind === "del") {
4339
+ result.push({ kind: "del", oldLine: op.oi + 1, text: oldLines[op.oi] });
4340
+ } else {
4341
+ result.push({ kind: "add", newLine: op.ni + 1, text: newLines[op.ni] });
4342
+ }
4343
+ }
4344
+ return result;
4345
+ }
4346
+
4081
4347
  // src/cli/ui/formatter.ts
4082
4348
  function renderGroupyBanner(info) {
4083
4349
  CliFormatter.printBanner(info);
4084
4350
  }
4085
- function formatToolCard(toolName, args, output, isError = false) {
4086
- CliFormatter.formatToolCall(toolName, args);
4087
- if (output !== undefined) {
4088
- CliFormatter.formatToolOutput(output, isError);
4351
+ function formatTurnSummary(metrics) {
4352
+ CliFormatter.formatTurnSummary(metrics);
4353
+ }
4354
+ function formatTaskStepStart(step, toolName, args) {
4355
+ const argsSummary = Object.entries(args).map(([k, v]) => {
4356
+ const valStr = typeof v === "string" ? `"${v.length > 35 ? v.slice(0, 32) + "..." : v}"` : JSON.stringify(v);
4357
+ return `${style.dim(k)}=${style.cyan(valStr)}`;
4358
+ }).join(" ");
4359
+ console.log(` ${style.brand("\u280B")} ${style.bold(`[${step}]`)} ${style.cyan(toolName)} ${argsSummary}`);
4360
+ }
4361
+ function formatTaskStepFinish(step, toolName, args, output, isError = false) {
4362
+ const icon = isError ? style.red("\u2717") : style.green("\u2714");
4363
+ const toolNameDisplay = style.bold(toolName);
4364
+ if (toolName === "apply_patch" && typeof args.targetContent === "string" && typeof args.replacementContent === "string") {
4365
+ const targetFile = args.path ? String(args.path) : undefined;
4366
+ console.log(` ${icon} ${style.dim(`[${step}]`)} ${toolNameDisplay} ${style.dim(targetFile || "")}`);
4367
+ CliFormatter.formatPatchDiff(targetFile, args.targetContent, args.replacementContent);
4368
+ return;
4369
+ }
4370
+ let summary = "";
4371
+ if (output) {
4372
+ const trimmed = output.trim();
4373
+ const lines = trimmed.split(`
4374
+ `);
4375
+ if (lines.length === 1 && lines[0].length <= 60) {
4376
+ summary = ` ${style.dim("\u21B3")} ${style.dim(lines[0])}`;
4377
+ } else if (toolName === "read_file" || toolName === "read_file_range") {
4378
+ summary = ` ${style.dim(`\u21B3 (${lines.length} lines read)`)}`;
4379
+ } else if (toolName === "grep_search" || toolName === "find_files") {
4380
+ summary = ` ${style.dim(`\u21B3 (${lines.length} items found)`)}`;
4381
+ } else {
4382
+ summary = ` ${style.dim(`\u21B3 (${lines.length} lines output)`)}`;
4383
+ }
4384
+ }
4385
+ console.log(` ${icon} ${style.dim(`[${step}]`)} ${toolNameDisplay}${summary}`);
4386
+ if (isError && output) {
4387
+ const firstLine = output.trim().split(`
4388
+ `)[0] || output;
4389
+ console.log(` ${style.red(firstLine.slice(0, 120))}`);
4089
4390
  }
4090
4391
  }
4091
-
4092
4392
  class CliFormatter {
4093
4393
  static printBanner(info) {
4094
4394
  const b = c.brandBold;
4095
4395
  const r = c.reset;
4096
4396
  const g = c.dim;
4097
4397
  const t = c.bold;
4398
+ const userDisplay = info.user ? style.cyan(info.user) : style.dim("Guest");
4098
4399
  console.log();
4099
4400
  console.log(` ${b} \u2584\u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2584\u2584${r}`);
4100
4401
  console.log(` ${b} \u2584\u2588\u2588\u2588\u2580\u2580 \u2580\u2580\u2588\u2588\u2588\u2584${r}`);
4101
4402
  console.log(` ${b} \u2584\u2588\u2588\u2580 \u2580\u2588\u2588\u2584${r} ${t}PIKAA AGENT${r}`);
4102
4403
  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)}`);
4404
+ console.log(` ${b}\u2588\u2588\u2588 \u2584\u2588\u2588\u2580 \u2580\u2588\u2588\u2584 \u2580\u2580${r} ${style.dim("User:")} ${userDisplay}`);
4405
+ 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
4406
  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
4407
  console.log(` ${b}\u2588\u2588\u2588 \u2580\u2588\u2588\u2584 \u2584\u2588\u2588\u2580 \u2584\u2584${r}`);
4107
4408
  console.log(` ${b} \u2588\u2588\u2588 \u2580\u2580\u2588\u2588\u2588\u2588\u2580\u2580 \u2588\u2588\u2588${r}`);
@@ -4119,6 +4420,12 @@ class CliFormatter {
4119
4420
  }).join(" ");
4120
4421
  console.log(` ${style.brand("\u25C6")} ${style.bold("tool:")} ${style.cyan(toolName)} ${argsSummary}`);
4121
4422
  }
4423
+ static formatPatchDiff(filePath, oldSrc, newSrc) {
4424
+ const lines = parsePatch(oldSrc, newSrc);
4425
+ if (lines.length === 0)
4426
+ return;
4427
+ console.log(renderDiff(lines, { filePath }));
4428
+ }
4122
4429
  static formatToolOutput(output, isError = false) {
4123
4430
  const prefix = isError ? style.red(" \u2717 error:") : style.dim(" \u21B3 output:");
4124
4431
  const lines = output.trim().split(`
@@ -4130,6 +4437,62 @@ class CliFormatter {
4130
4437
  console.log(`${prefix}
4131
4438
  ${preview}${more}`);
4132
4439
  }
4440
+ static formatTurnSummary(metrics) {
4441
+ const formatTokens = (n) => {
4442
+ if (n >= 1e6)
4443
+ return `${(n / 1e6).toFixed(1)}M`;
4444
+ if (n >= 1000) {
4445
+ const val = (n / 1000).toFixed(1);
4446
+ return val.endsWith(".0") ? `${val.slice(0, -2)}k` : `${val}k`;
4447
+ }
4448
+ return String(n);
4449
+ };
4450
+ const durationSec = (metrics.durationMs / 1000).toFixed(1);
4451
+ const parts = [`${c.bold}${durationSec}s${c.reset}`];
4452
+ if (metrics.inputTokens !== undefined || metrics.outputTokens !== undefined) {
4453
+ const inStr = metrics.inputTokens !== undefined ? formatTokens(metrics.inputTokens) : "0";
4454
+ const outStr = metrics.outputTokens !== undefined ? formatTokens(metrics.outputTokens) : "0";
4455
+ parts.push(`${style.cyan(`${inStr} in`)} ${style.dim("/")} ${style.cyan(`${outStr} out`)}`);
4456
+ } else if (metrics.totalTokens !== undefined && metrics.totalTokens > 0) {
4457
+ parts.push(`${style.dim(`${formatTokens(metrics.totalTokens)} tokens`)}`);
4458
+ }
4459
+ if (metrics.contextTokens !== undefined && metrics.maxContextTokens !== undefined && metrics.maxContextTokens > 0) {
4460
+ const pct = Math.round(metrics.contextTokens / metrics.maxContextTokens * 100);
4461
+ const colorFn = pct < 50 ? style.green : pct < 80 ? style.yellow : style.red;
4462
+ const ctxLabel = colorFn(`${pct}% context`);
4463
+ parts.push(ctxLabel);
4464
+ }
4465
+ if (metrics.toolCalls && metrics.toolCalls.length > 0) {
4466
+ const count = metrics.toolCalls.length;
4467
+ const uniqueTools = Array.from(new Set(metrics.toolCalls));
4468
+ const toolLabel = count === 1 ? `1 tool (${uniqueTools.join(", ")})` : `${count} tools (${uniqueTools.join(", ")})`;
4469
+ parts.push(`${style.cyan(toolLabel)}`);
4470
+ }
4471
+ if (metrics.filesModified && metrics.filesModified.length > 0) {
4472
+ const count = metrics.filesModified.length;
4473
+ const fileLabel = count === 1 ? `1 file updated` : `${count} files updated`;
4474
+ parts.push(`${style.green(fileLabel)}`);
4475
+ }
4476
+ const dot = `${style.dim(" \xB7 ")}`;
4477
+ console.log(`
4478
+ ${c.brandBold}\u273B${c.reset} ${style.dim("Completed in")} ${parts.join(dot)}`);
4479
+ const sessionParts = [];
4480
+ if (metrics.sessionUptimeMs !== undefined && metrics.sessionUptimeMs >= 1000) {
4481
+ sessionParts.push(`${style.dim("session:")} ${style.bold(formatDuration(metrics.sessionUptimeMs))}`);
4482
+ }
4483
+ if (metrics.subAgents && metrics.subAgents.length > 0) {
4484
+ const agentBadges = metrics.subAgents.map((a) => {
4485
+ const timeStr = formatDuration(a.runningTimeMs);
4486
+ const icon = a.status === "running" ? style.brand("\u25CF") : a.status === "completed" ? style.green("\u2714") : style.red("\u2717");
4487
+ return `${icon} ${style.cyan(a.nickname)} ${style.dim(`(${a.role}, ${timeStr})`)}`;
4488
+ });
4489
+ sessionParts.push(`${style.dim("sub-agents:")} [${agentBadges.join(", ")}]`);
4490
+ }
4491
+ if (sessionParts.length > 0) {
4492
+ console.log(` ${style.dim("\u21B3")} ${sessionParts.join(dot)}`);
4493
+ }
4494
+ console.log();
4495
+ }
4133
4496
  static formatMarkdownLine(chunk) {
4134
4497
  let formatted = chunk.replace(/\*\*(.*?)\*\*/g, `${c.bold}$1${c.reset}`);
4135
4498
  formatted = formatted.replace(/`([^`]+)`/g, `${c.cyan}$1${c.reset}`);
@@ -4158,9 +4521,12 @@ function ensureKeypressInitialized() {
4158
4521
 
4159
4522
  class InteractiveLineEditor {
4160
4523
  promptSymbol;
4524
+ cwd;
4161
4525
  onInterrupt;
4526
+ searchEngine = new FileSearchEngine;
4162
4527
  constructor(options = {}) {
4163
4528
  this.promptSymbol = options.promptSymbol || `${c.brandBold}\u276F${c.reset} `;
4529
+ this.cwd = options.cwd || process.cwd();
4164
4530
  this.onInterrupt = options.onInterrupt;
4165
4531
  }
4166
4532
  async readLine() {
@@ -4178,6 +4544,7 @@ class InteractiveLineEditor {
4178
4544
  let buffer = "";
4179
4545
  let cursor = 0;
4180
4546
  let selectedIndex = 0;
4547
+ let scrollTop = 0;
4181
4548
  let renderedMenuLines = 0;
4182
4549
  let popupDismissed = false;
4183
4550
  if (!globalRawMode && process.stdin.isTTY) {
@@ -4194,6 +4561,47 @@ class InteractiveLineEditor {
4194
4561
  const matches = AVAILABLE_SLASH_COMMANDS.filter((cmd) => cmd.name.startsWith(term));
4195
4562
  return matches.length > 0 ? matches : AVAILABLE_SLASH_COMMANDS;
4196
4563
  };
4564
+ const getActiveFileQuery = () => {
4565
+ if (popupDismissed)
4566
+ return null;
4567
+ const beforeCursor = buffer.slice(0, cursor);
4568
+ const match = beforeCursor.match(/(?:^|\s)@([^\s]*)$/);
4569
+ if (!match)
4570
+ return null;
4571
+ const query = match[1] ?? "";
4572
+ const atIndex = beforeCursor.lastIndexOf("@");
4573
+ return { query, atIndex };
4574
+ };
4575
+ const getMatchingFiles = (fileQuery) => {
4576
+ try {
4577
+ return this.searchEngine.findFiles(this.cwd, {
4578
+ pattern: fileQuery,
4579
+ maxResults: 30
4580
+ });
4581
+ } catch {
4582
+ return [];
4583
+ }
4584
+ };
4585
+ const ensureVisible = (totalItems, visibleRows) => {
4586
+ if (totalItems === 0 || visibleRows === 0) {
4587
+ scrollTop = 0;
4588
+ return;
4589
+ }
4590
+ if (selectedIndex < 0)
4591
+ selectedIndex = 0;
4592
+ if (selectedIndex >= totalItems)
4593
+ selectedIndex = totalItems - 1;
4594
+ if (selectedIndex < scrollTop) {
4595
+ scrollTop = selectedIndex;
4596
+ } else if (selectedIndex >= scrollTop + visibleRows) {
4597
+ scrollTop = selectedIndex + 1 - visibleRows;
4598
+ }
4599
+ if (scrollTop < 0)
4600
+ scrollTop = 0;
4601
+ const maxScroll = Math.max(0, totalItems - visibleRows);
4602
+ if (scrollTop > maxScroll)
4603
+ scrollTop = maxScroll;
4604
+ };
4197
4605
  const clearMenu = () => {
4198
4606
  if (renderedMenuLines > 0) {
4199
4607
  for (let i = 0;i < renderedMenuLines; i++) {
@@ -4207,28 +4615,79 @@ class InteractiveLineEditor {
4207
4615
  const redraw = () => {
4208
4616
  clearMenu();
4209
4617
  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;
4618
+ const slashMatches = getMatchingCommands();
4619
+ const activeFile = getActiveFileQuery();
4620
+ const fileMatches = activeFile ? getMatchingFiles(activeFile.query) : [];
4621
+ if (buffer.startsWith("/") && !popupDismissed && slashMatches.length > 0) {
4622
+ const BOX_WIDTH = 70;
4623
+ const maxVisible = Math.min(slashMatches.length, 7);
4624
+ ensureVisible(slashMatches.length, maxVisible);
4625
+ const visibleMatches = slashMatches.slice(scrollTop, scrollTop + maxVisible);
4217
4626
  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;
4627
+ menuLines.push(` ${style.dim("\u250C" + "\u2500".repeat(BOX_WIDTH) + "\u2510")}`);
4628
+ for (let i = 0;i < visibleMatches.length; i++) {
4629
+ const cmd = visibleMatches[i];
4630
+ const actualIdx = scrollTop + i;
4631
+ const isSelected = actualIdx === selectedIndex;
4222
4632
  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")}`);
4633
+ const rawName = cmd.name.padEnd(13).slice(0, 13);
4634
+ const rawDesc = cmd.description.length > 50 ? cmd.description.slice(0, 47) + "..." : cmd.description.padEnd(50);
4635
+ const coloredName = isSelected ? style.brandBold(rawName) : style.cyan(rawName);
4636
+ const coloredDesc = isSelected ? style.bold(rawDesc) : style.dim(rawDesc);
4637
+ menuLines.push(` ${style.dim("\u2502")} ${marker} ${coloredName} ${coloredDesc} ${style.dim("\u2502")}`);
4226
4638
  }
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")}`);
4639
+ let footerMsg = "";
4640
+ const moreAbove = scrollTop;
4641
+ const moreBelow = Math.max(0, slashMatches.length - (scrollTop + visibleMatches.length));
4642
+ if (moreAbove > 0 && moreBelow > 0) {
4643
+ footerMsg = ` ... ${moreAbove} more above, ${moreBelow} more below (\u2191/\u2193 to scroll)`;
4644
+ } else if (moreBelow > 0) {
4645
+ footerMsg = ` ... and ${moreBelow} more commands (use arrows to scroll)`;
4646
+ } else if (moreAbove > 0) {
4647
+ footerMsg = ` ... and ${moreAbove} more commands above (use arrows to scroll)`;
4648
+ } else {
4649
+ footerMsg = ` ${slashMatches.length} commands (\u2191/\u2193 to navigate \u2022 Tab to select)`;
4230
4650
  }
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")}`);
4651
+ const footerPadded = footerMsg.padEnd(BOX_WIDTH).slice(0, BOX_WIDTH);
4652
+ menuLines.push(` ${style.dim("\u2502")}${style.dim(footerPadded)}${style.dim("\u2502")}`);
4653
+ menuLines.push(` ${style.dim("\u2514" + "\u2500".repeat(BOX_WIDTH) + "\u2518")}`);
4654
+ for (const line of menuLines) {
4655
+ process.stdout.write(`
4656
+ \x1B[2K${line}`);
4657
+ }
4658
+ renderedMenuLines = menuLines.length;
4659
+ process.stdout.write(`\x1B[${renderedMenuLines}A`);
4660
+ } else if (activeFile && !popupDismissed && fileMatches.length > 0) {
4661
+ const BOX_WIDTH = 70;
4662
+ const maxVisible = Math.min(fileMatches.length, 7);
4663
+ ensureVisible(fileMatches.length, maxVisible);
4664
+ const visibleMatches = fileMatches.slice(scrollTop, scrollTop + maxVisible);
4665
+ const menuLines = [];
4666
+ menuLines.push(` ${style.dim("\u250C\u2500\u2500")} ${style.brandBold("Files")} ${style.dim("\u2500".repeat(Math.max(10, BOX_WIDTH - 9)) + "\u2510")}`);
4667
+ for (let i = 0;i < visibleMatches.length; i++) {
4668
+ const filePath = visibleMatches[i];
4669
+ const actualIdx = scrollTop + i;
4670
+ const isSelected = actualIdx === selectedIndex;
4671
+ const marker = isSelected ? style.brand("\u276F") : " ";
4672
+ const rawPath = filePath.length > 62 ? "..." + filePath.slice(filePath.length - 59) : filePath.padEnd(62);
4673
+ const coloredPath = isSelected ? style.brandBold(rawPath) : style.cyan(rawPath);
4674
+ menuLines.push(` ${style.dim("\u2502")} ${marker} ${coloredPath} ${style.dim("\u2502")}`);
4675
+ }
4676
+ let footerMsg = "";
4677
+ const moreAbove = scrollTop;
4678
+ const moreBelow = Math.max(0, fileMatches.length - (scrollTop + visibleMatches.length));
4679
+ if (moreAbove > 0 && moreBelow > 0) {
4680
+ footerMsg = ` ... ${moreAbove} more above, ${moreBelow} more below (\u2191/\u2193 to scroll)`;
4681
+ } else if (moreBelow > 0) {
4682
+ footerMsg = ` ... and ${moreBelow} more files (use arrows \u2022 Tab to insert)`;
4683
+ } else if (moreAbove > 0) {
4684
+ footerMsg = ` ... and ${moreAbove} more files above (use arrows \u2022 Tab to insert)`;
4685
+ } else {
4686
+ footerMsg = ` ${fileMatches.length} files (\u2191/\u2193 to navigate \u2022 Tab to insert)`;
4687
+ }
4688
+ const footerPadded = footerMsg.padEnd(BOX_WIDTH).slice(0, BOX_WIDTH);
4689
+ menuLines.push(` ${style.dim("\u2502")}${style.dim(footerPadded)}${style.dim("\u2502")}`);
4690
+ menuLines.push(` ${style.dim("\u2514" + "\u2500".repeat(BOX_WIDTH) + "\u2518")}`);
4232
4691
  for (const line of menuLines) {
4233
4692
  process.stdout.write(`
4234
4693
  \x1B[2K${line}`);
@@ -4259,6 +4718,7 @@ class InteractiveLineEditor {
4259
4718
  buffer = "";
4260
4719
  cursor = 0;
4261
4720
  selectedIndex = 0;
4721
+ scrollTop = 0;
4262
4722
  popupDismissed = false;
4263
4723
  redraw();
4264
4724
  return;
@@ -4290,10 +4750,24 @@ class InteractiveLineEditor {
4290
4750
  cleanupAndResolve("/exit");
4291
4751
  return;
4292
4752
  }
4753
+ const slashMatches = getMatchingCommands();
4754
+ const activeFile = getActiveFileQuery();
4755
+ const fileMatches = activeFile ? getMatchingFiles(activeFile.query) : [];
4293
4756
  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];
4757
+ if (activeFile && !popupDismissed && fileMatches.length > 0) {
4758
+ const chosen = fileMatches[selectedIndex] || fileMatches[0];
4759
+ const beforeAt = buffer.slice(0, activeFile.atIndex);
4760
+ const afterCursor = buffer.slice(cursor);
4761
+ buffer = `${beforeAt}@${chosen} ${afterCursor}`;
4762
+ cursor = beforeAt.length + chosen.length + 2;
4763
+ selectedIndex = 0;
4764
+ scrollTop = 0;
4765
+ popupDismissed = false;
4766
+ redraw();
4767
+ return;
4768
+ }
4769
+ if (buffer.startsWith("/") && !popupDismissed && slashMatches.length > 0 && selectedIndex >= 0 && selectedIndex < slashMatches.length) {
4770
+ const selected = slashMatches[selectedIndex];
4297
4771
  cleanupAndResolve(selected.name);
4298
4772
  return;
4299
4773
  }
@@ -4301,13 +4775,25 @@ class InteractiveLineEditor {
4301
4775
  return;
4302
4776
  }
4303
4777
  if (key.name === "tab") {
4304
- const matches = getMatchingCommands();
4305
- if (matches.length > 0) {
4306
- const match = matches[selectedIndex] || matches[0];
4778
+ if (activeFile && !popupDismissed && fileMatches.length > 0) {
4779
+ const chosen = fileMatches[selectedIndex] || fileMatches[0];
4780
+ const beforeAt = buffer.slice(0, activeFile.atIndex);
4781
+ const afterCursor = buffer.slice(cursor);
4782
+ buffer = `${beforeAt}@${chosen} ${afterCursor}`;
4783
+ cursor = beforeAt.length + chosen.length + 2;
4784
+ selectedIndex = 0;
4785
+ scrollTop = 0;
4786
+ popupDismissed = false;
4787
+ redraw();
4788
+ return;
4789
+ }
4790
+ if (slashMatches.length > 0) {
4791
+ const match = slashMatches[selectedIndex] || slashMatches[0];
4307
4792
  if (match) {
4308
4793
  buffer = match.name + " ";
4309
4794
  cursor = buffer.length;
4310
4795
  selectedIndex = 0;
4796
+ scrollTop = 0;
4311
4797
  popupDismissed = false;
4312
4798
  redraw();
4313
4799
  }
@@ -4315,17 +4801,25 @@ class InteractiveLineEditor {
4315
4801
  return;
4316
4802
  }
4317
4803
  if (key.name === "up") {
4318
- const matches = getMatchingCommands();
4319
- if (matches.length > 0) {
4320
- selectedIndex = (selectedIndex - 1 + matches.length) % matches.length;
4804
+ if (activeFile && fileMatches.length > 0) {
4805
+ selectedIndex = (selectedIndex - 1 + fileMatches.length) % fileMatches.length;
4806
+ redraw();
4807
+ return;
4808
+ }
4809
+ if (slashMatches.length > 0) {
4810
+ selectedIndex = (selectedIndex - 1 + slashMatches.length) % slashMatches.length;
4321
4811
  redraw();
4322
4812
  return;
4323
4813
  }
4324
4814
  }
4325
4815
  if (key.name === "down") {
4326
- const matches = getMatchingCommands();
4327
- if (matches.length > 0) {
4328
- selectedIndex = (selectedIndex + 1) % matches.length;
4816
+ if (activeFile && fileMatches.length > 0) {
4817
+ selectedIndex = (selectedIndex + 1) % fileMatches.length;
4818
+ redraw();
4819
+ return;
4820
+ }
4821
+ if (slashMatches.length > 0) {
4822
+ selectedIndex = (selectedIndex + 1) % slashMatches.length;
4329
4823
  redraw();
4330
4824
  return;
4331
4825
  }
@@ -4349,6 +4843,7 @@ class InteractiveLineEditor {
4349
4843
  buffer = buffer.slice(0, cursor - 1) + buffer.slice(cursor);
4350
4844
  cursor--;
4351
4845
  selectedIndex = 0;
4846
+ scrollTop = 0;
4352
4847
  popupDismissed = false;
4353
4848
  redraw();
4354
4849
  }
@@ -4358,6 +4853,7 @@ class InteractiveLineEditor {
4358
4853
  buffer = buffer.slice(0, cursor) + _str + buffer.slice(cursor);
4359
4854
  cursor += _str.length;
4360
4855
  selectedIndex = 0;
4856
+ scrollTop = 0;
4361
4857
  popupDismissed = false;
4362
4858
  redraw();
4363
4859
  }
@@ -4371,6 +4867,7 @@ class InteractiveLineEditor {
4371
4867
  // src/cli/commands.ts
4372
4868
  var AVAILABLE_SLASH_COMMANDS = [
4373
4869
  { name: "/help", description: "Show command list and help menu" },
4870
+ { name: "/stats", description: "Display session runtime, turn stats & sub-agent status" },
4374
4871
  { name: "/model", description: "Select or switch active AI model" },
4375
4872
  { name: "/models", description: "List available AI models from gateway" },
4376
4873
  { name: "/reasoning", description: "Toggle internal reasoning chain visibility" },
@@ -4397,6 +4894,11 @@ async function handleSlashCommand(input, ctx) {
4397
4894
  case "/help":
4398
4895
  printHelp();
4399
4896
  return true;
4897
+ case "/stats":
4898
+ case "/time":
4899
+ case "/uptime":
4900
+ printSessionStats(ctx);
4901
+ return true;
4400
4902
  case "/model":
4401
4903
  case "/models":
4402
4904
  await handleModelSelection(ctx, args[0]);
@@ -4736,11 +5238,170 @@ function printSessions(ctx) {
4736
5238
  }
4737
5239
  console.log();
4738
5240
  }
5241
+ function printSessionStats(ctx) {
5242
+ const uptimeMs = ctx.repl ? Date.now() - ctx.repl.sessionStartTime : 0;
5243
+ const turns = ctx.repl?.turnCount ?? 0;
5244
+ const history = ctx.session.getHistory();
5245
+ const historyTokens = estimateTotalTokens(history);
5246
+ const maxTokens = 128000;
5247
+ const contextPct = Math.round(historyTokens / maxTokens * 100);
5248
+ const colorFn = contextPct < 50 ? style.green : contextPct < 80 ? style.yellow : style.red;
5249
+ const agents = ctx.spawner?.listAgents() || [];
5250
+ const activeAgents = agents.filter((a) => a.status === "running");
5251
+ console.log();
5252
+ 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"));
5253
+ console.log(` \u2502 ${style.dim("Uptime:")} ${style.bold(formatDuration(uptimeMs))}`);
5254
+ console.log(` \u2502 ${style.dim("Turns:")} ${turns} completed`);
5255
+ console.log(` \u2502 ${style.dim("History Items:")} ${history.length} items`);
5256
+ console.log(` \u2502 ${style.dim("Context Usage:")} ${colorFn(`${contextPct}%`)} ${style.dim(`(~${historyTokens} tokens / ${maxTokens} max)`)}`);
5257
+ console.log(` \u2502 ${style.dim("Model:")} ${style.brand(ctx.session.model)}`);
5258
+ console.log(` \u2502 ${style.dim("Working Dir:")} ${style.dim(ctx.session.cwd)}`);
5259
+ console.log(" \u2502");
5260
+ if (agents.length === 0) {
5261
+ console.log(` \u2502 ${style.dim("Sub-agents:")} None spawned yet`);
5262
+ } else {
5263
+ console.log(` \u2502 ${style.bold("Sub-agents:")} (${activeAgents.length} active, ${agents.length - activeAgents.length} completed)`);
5264
+ for (const a of agents) {
5265
+ const runtime = formatDuration(Date.now() - a.createdAt);
5266
+ const icon = a.status === "running" ? style.brand("\u25CF") : a.status === "completed" ? style.green("\u2714") : style.red("\u2717");
5267
+ console.log(` \u2502 ${icon} ${style.cyan(a.nickname)} [${style.dim(a.role)}] - ${a.status} (${runtime})`);
5268
+ console.log(` \u2502 ${style.dim(`Task: ${a.taskName}`)}`);
5269
+ }
5270
+ }
5271
+ 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"));
5272
+ console.log();
5273
+ }
5274
+
5275
+ // src/cli/ui/prompt.ts
5276
+ import readline2 from "readline";
5277
+ async function promptChoice(config) {
5278
+ const { message, choices, defaultIndex = 0 } = config;
5279
+ if (!process.stdin.isTTY) {
5280
+ if (!process.stdin.readable) {
5281
+ return choices[defaultIndex]?.value ?? choices[0].value;
5282
+ }
5283
+ return new Promise((resolve13) => {
5284
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
5285
+ const hint = choices.map((c3) => c3.isDefault ? `[${c3.key.toUpperCase()}]` : `[${c3.key}]`).join("/");
5286
+ let resolved = false;
5287
+ const doResolve = (val) => {
5288
+ if (!resolved) {
5289
+ resolved = true;
5290
+ rl.close();
5291
+ resolve13(val);
5292
+ }
5293
+ };
5294
+ rl.question(`${message} ${hint}: `, (answer) => {
5295
+ const trimmed = answer.trim().toLowerCase();
5296
+ const matched = choices.find((c3) => c3.key.toLowerCase() === trimmed);
5297
+ doResolve(matched ? matched.value : choices[defaultIndex].value);
5298
+ });
5299
+ rl.on("close", () => {
5300
+ doResolve(choices[defaultIndex].value);
5301
+ });
5302
+ });
5303
+ }
5304
+ return new Promise((resolve13) => {
5305
+ let selectedIndex = defaultIndex;
5306
+ if (selectedIndex < 0 || selectedIndex >= choices.length)
5307
+ selectedIndex = 0;
5308
+ readline2.emitKeypressEvents(process.stdin);
5309
+ const wasRaw = process.stdin.isRaw;
5310
+ try {
5311
+ process.stdin.setRawMode(true);
5312
+ } catch {}
5313
+ process.stdin.resume();
5314
+ const render = () => {
5315
+ const parts = choices.map((choice, idx) => {
5316
+ const isSelected = idx === selectedIndex;
5317
+ const keyTag = `[${choice.key}]`;
5318
+ if (isSelected) {
5319
+ return `${c.bgBrand}${c.white}${c.bold} \u276F ${keyTag} ${choice.label} ${c.reset}`;
5320
+ }
5321
+ return `${style.dim(` ${keyTag} ${choice.label}`)}`;
5322
+ });
5323
+ process.stdout.write(`\r\x1B[2K ${style.bold(message)}
5324
+ \r\x1B[2K${parts.join(" ")}`);
5325
+ };
5326
+ const cleanup = (confirmedChoice) => {
5327
+ process.stdin.removeListener("keypress", onKeypress);
5328
+ try {
5329
+ process.stdin.setRawMode(wasRaw ?? false);
5330
+ } catch {}
5331
+ process.stdout.write(`\r\x1B[1A\r\x1B[2K ${style.bold(message)} ${style.cyan(`[${confirmedChoice.key}] ${confirmedChoice.label}`)}
5332
+ \r\x1B[2K`);
5333
+ resolve13(confirmedChoice.value);
5334
+ };
5335
+ const onKeypress = (_str, key) => {
5336
+ if (!key)
5337
+ return;
5338
+ if (key.ctrl && key.name === "c") {
5339
+ cleanup(choices.find((c3) => c3.key.toLowerCase() === "n") || choices[defaultIndex]);
5340
+ return;
5341
+ }
5342
+ if (key.name === "escape") {
5343
+ cleanup(choices.find((c3) => c3.key.toLowerCase() === "n") || choices[defaultIndex]);
5344
+ return;
5345
+ }
5346
+ if (key.name === "return" || key.name === "enter") {
5347
+ cleanup(choices[selectedIndex]);
5348
+ return;
5349
+ }
5350
+ if (key.name === "left") {
5351
+ selectedIndex = (selectedIndex - 1 + choices.length) % choices.length;
5352
+ process.stdout.write("\x1B[1A");
5353
+ render();
5354
+ return;
5355
+ }
5356
+ if (key.name === "right" || key.name === "tab") {
5357
+ selectedIndex = (selectedIndex + 1) % choices.length;
5358
+ process.stdout.write("\x1B[1A");
5359
+ render();
5360
+ return;
5361
+ }
5362
+ const char = _str ? _str.toLowerCase() : key.name ? key.name.toLowerCase() : "";
5363
+ if (char) {
5364
+ const directMatch = choices.find((c3) => c3.key.toLowerCase() === char);
5365
+ if (directMatch) {
5366
+ cleanup(directMatch);
5367
+ return;
5368
+ }
5369
+ }
5370
+ };
5371
+ process.stdin.on("keypress", onKeypress);
5372
+ render();
5373
+ });
5374
+ }
5375
+ async function promptToolApproval(params) {
5376
+ const boxWidth = Math.min(process.stdout.columns ?? 80, 70);
5377
+ const border = "\u2500".repeat(Math.max(10, boxWidth - 24));
5378
+ console.log(`
5379
+ ${style.yellow("\u250C\u2500\u2500")} ${style.bold("Approval Required")} ${style.yellow(border)}`);
5380
+ console.log(` ${style.yellow("\u2502")} ${style.dim("Tool:")} ${style.bold(params.toolName)}`);
5381
+ if (params.command) {
5382
+ console.log(` ${style.yellow("\u2502")} ${style.dim("Command:")} ${style.cyan(params.command)}`);
5383
+ }
5384
+ console.log(` ${style.yellow("\u2502")} ${style.dim("Reason:")} ${style.dim(params.description)}`);
5385
+ console.log(` ${style.yellow("\u2514" + "\u2500".repeat(Math.max(10, boxWidth - 4)))}
5386
+ `);
5387
+ const decision = await promptChoice({
5388
+ message: "Allow execution?",
5389
+ choices: [
5390
+ { key: "y", label: "Yes", value: "yes", isDefault: true },
5391
+ { key: "n", label: "No", value: "no" },
5392
+ { key: "a", label: "Always allow this session", value: "always" }
5393
+ ],
5394
+ defaultIndex: 0
5395
+ });
5396
+ return decision;
5397
+ }
4739
5398
 
4740
5399
  // src/cli/ui/markdown.ts
4741
5400
  class MarkdownHighlighter {
4742
5401
  inCodeBlock = false;
4743
5402
  currentLanguage = "";
5403
+ diffBuffer = [];
5404
+ inDiffBlock = false;
4744
5405
  lineBuffer = "";
4745
5406
  static highlight(markdown) {
4746
5407
  const highlighter = new MarkdownHighlighter;
@@ -4774,17 +5435,53 @@ class MarkdownHighlighter {
4774
5435
  if (!this.inCodeBlock) {
4775
5436
  this.inCodeBlock = true;
4776
5437
  this.currentLanguage = fenceMatch[1] || "";
5438
+ if (this.currentLanguage.toLowerCase() === "diff") {
5439
+ this.inDiffBlock = true;
5440
+ this.diffBuffer = [];
5441
+ return "";
5442
+ }
4777
5443
  const langBadge = this.currentLanguage ? ` ${style.brandBold(this.currentLanguage.toUpperCase())} ` : "";
4778
5444
  return `
4779
5445
  ${style.dim("\u250C\u2500\u2500")}${langBadge}${style.dim("\u2500".repeat(Math.max(10, 60 - (this.currentLanguage.length + 6))))}`;
4780
5446
  } else {
4781
5447
  this.inCodeBlock = false;
4782
5448
  this.currentLanguage = "";
5449
+ if (this.inDiffBlock) {
5450
+ this.inDiffBlock = false;
5451
+ const raw = this.diffBuffer.join(`
5452
+ `);
5453
+ this.diffBuffer = [];
5454
+ const oldLines = [];
5455
+ const newLines = [];
5456
+ for (const l of raw.split(`
5457
+ `)) {
5458
+ if (l.startsWith("-")) {
5459
+ oldLines.push(l.slice(1));
5460
+ newLines.push("");
5461
+ } else if (l.startsWith("+")) {
5462
+ oldLines.push("");
5463
+ newLines.push(l.slice(1));
5464
+ } else {
5465
+ oldLines.push(l.startsWith(" ") ? l.slice(1) : l);
5466
+ newLines.push(l.startsWith(" ") ? l.slice(1) : l);
5467
+ }
5468
+ }
5469
+ const diffLines = parsePatch(oldLines.join(`
5470
+ `), newLines.join(`
5471
+ `), 3);
5472
+ return `
5473
+ ` + renderDiff(diffLines) + `
5474
+ `;
5475
+ }
4783
5476
  return ` ${style.dim("\u2514" + "\u2500".repeat(60))}
4784
5477
  `;
4785
5478
  }
4786
5479
  }
4787
5480
  if (this.inCodeBlock) {
5481
+ if (this.inDiffBlock) {
5482
+ this.diffBuffer.push(line);
5483
+ return "";
5484
+ }
4788
5485
  const highlightedCode = this.highlightCode(line, this.currentLanguage);
4789
5486
  return ` ${style.dim("\u2502")} ${highlightedCode}`;
4790
5487
  }
@@ -4941,6 +5638,8 @@ ${style.bold(c.brightCyan + text.replace(/^##\s+/, "\u25A0 "))}${c.reset}`;
4941
5638
  // src/cli/repl.ts
4942
5639
  class CliRepl {
4943
5640
  showReasoning = false;
5641
+ sessionStartTime = Date.now();
5642
+ turnCount = 0;
4944
5643
  session;
4945
5644
  spawner;
4946
5645
  mcpManager;
@@ -4956,6 +5655,11 @@ class CliRepl {
4956
5655
  isClosed = false;
4957
5656
  highlighter = new MarkdownHighlighter;
4958
5657
  turnDoneResolver;
5658
+ turnStartTime = 0;
5659
+ turnToolCalls = [];
5660
+ turnFilesModified = new Set;
5661
+ turnCharsOut = 0;
5662
+ activeToolArgs = {};
4959
5663
  constructor(options) {
4960
5664
  this.session = options.session;
4961
5665
  this.spawner = options.spawner;
@@ -4974,9 +5678,13 @@ class CliRepl {
4974
5678
  switch (msg.type) {
4975
5679
  case "TurnStarted":
4976
5680
  this.isProcessing = true;
5681
+ this.turnCount++;
4977
5682
  this.currentTurnHasOutput = false;
4978
5683
  this.reasoningStarted = false;
4979
- this.highlighter = new MarkdownHighlighter;
5684
+ this.turnStartTime = performance.now();
5685
+ this.turnToolCalls = [];
5686
+ this.turnFilesModified.clear();
5687
+ this.turnCharsOut = 0;
4980
5688
  this.spinner.start("Thinking...");
4981
5689
  break;
4982
5690
  case "ReasoningDelta":
@@ -4985,7 +5693,7 @@ class CliRepl {
4985
5693
  if (!this.reasoningStarted) {
4986
5694
  this.spinner.stop();
4987
5695
  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`));
5696
+ \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
5697
  this.reasoningStarted = true;
4990
5698
  }
4991
5699
  process.stdout.write(style.dim(msg.delta));
@@ -5004,6 +5712,7 @@ class CliRepl {
5004
5712
  this.spinner.stop();
5005
5713
  this.currentTurnHasOutput = true;
5006
5714
  }
5715
+ this.turnCharsOut += msg.delta.length;
5007
5716
  if (this.currentTurnHasOutput) {
5008
5717
  const formatted = this.highlighter.feed(msg.delta);
5009
5718
  if (formatted) {
@@ -5019,13 +5728,18 @@ class CliRepl {
5019
5728
  this.reasoningStarted = false;
5020
5729
  }
5021
5730
  this.spinner.stop();
5731
+ this.turnToolCalls.push(msg.toolName);
5732
+ this.activeToolArgs = msg.arguments || {};
5733
+ if ((msg.toolName === "apply_patch" || msg.toolName === "write_file") && msg.arguments && typeof msg.arguments.path === "string") {
5734
+ this.turnFilesModified.add(msg.arguments.path);
5735
+ }
5022
5736
  console.log();
5023
- formatToolCard(msg.toolName, msg.arguments);
5024
- this.spinner.start(`Executing ${msg.toolName}...`);
5737
+ formatTaskStepStart(this.turnToolCalls.length, msg.toolName, msg.arguments);
5738
+ this.spinner.start(`Executing [${this.turnToolCalls.length}] ${msg.toolName}...`);
5025
5739
  break;
5026
5740
  case "ToolCallFinished":
5027
5741
  this.spinner.stop();
5028
- formatToolCard(msg.toolName, {}, msg.output, msg.isError);
5742
+ formatTaskStepFinish(this.turnToolCalls.length, msg.toolName, this.activeToolArgs, msg.output, msg.isError);
5029
5743
  break;
5030
5744
  case "InteractiveApprovalRequired":
5031
5745
  case "ApprovalRequired":
@@ -5045,11 +5759,26 @@ class CliRepl {
5045
5759
  process.stdout.write(flushed);
5046
5760
  }
5047
5761
  this.isProcessing = false;
5048
- console.log();
5049
- if (msg.totalTokens) {
5050
- console.log(style.dim(`[Turn completed | ${msg.totalTokens} tokens]`));
5051
- }
5052
- console.log();
5762
+ const durationMs = this.turnStartTime > 0 ? performance.now() - this.turnStartTime : 0;
5763
+ const sessionUptimeMs = Date.now() - this.sessionStartTime;
5764
+ const subAgents = this.spawner?.listAgents().map((a) => ({
5765
+ nickname: a.nickname,
5766
+ role: a.role,
5767
+ status: a.status,
5768
+ runningTimeMs: Date.now() - a.createdAt
5769
+ }));
5770
+ formatTurnSummary({
5771
+ durationMs,
5772
+ inputTokens: msg.inputTokens,
5773
+ outputTokens: msg.outputTokens || (this.turnCharsOut > 0 ? Math.round(this.turnCharsOut / 3.8) : undefined),
5774
+ totalTokens: msg.totalTokens,
5775
+ contextTokens: msg.contextTokens,
5776
+ maxContextTokens: msg.maxContextTokens,
5777
+ sessionUptimeMs,
5778
+ subAgents,
5779
+ toolCalls: this.turnToolCalls,
5780
+ filesModified: Array.from(this.turnFilesModified)
5781
+ });
5053
5782
  if (this.turnDoneResolver) {
5054
5783
  const resolve13 = this.turnDoneResolver;
5055
5784
  this.turnDoneResolver = undefined;
@@ -5078,35 +5807,34 @@ class CliRepl {
5078
5807
  });
5079
5808
  }
5080
5809
  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
5810
  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) {
5811
+ const decision = await promptToolApproval(msg);
5812
+ if (decision === "always") {
5813
+ this.session.execPolicy.addRule(/.*/, "allow", "User allowed all actions for this session");
5814
+ this.session.resolveApproval(msg.approvalId, true);
5815
+ this.spinner.start(`Executing approved action (auto-approved for session)...`);
5816
+ } else if (decision === "yes") {
5817
+ this.session.resolveApproval(msg.approvalId, true);
5095
5818
  this.spinner.start(`Executing approved action...`);
5096
5819
  } else {
5097
- console.log(style.dim("Action rejected by user."));
5820
+ this.session.resolveApproval(msg.approvalId, false);
5821
+ console.log(style.dim(" Action rejected by user."));
5098
5822
  }
5099
5823
  } catch {
5100
5824
  this.session.resolveApproval(msg.approvalId, false);
5101
5825
  }
5102
5826
  }
5103
5827
  async start() {
5828
+ const creds = new CredentialsStore().load();
5829
+ const accountUser = creds?.user?.username || creds?.user?.email || (creds?.accessToken ? "Authenticated" : undefined);
5104
5830
  renderGroupyBanner({
5831
+ user: accountUser,
5105
5832
  role: this.role,
5106
5833
  model: this.session.model,
5107
5834
  cwd: this.session.cwd
5108
5835
  });
5109
5836
  const editor = new InteractiveLineEditor({
5837
+ cwd: this.session.cwd,
5110
5838
  onInterrupt: () => {
5111
5839
  if (this.isProcessing) {
5112
5840
  const activeTurn = this.session.getActiveTurn();
@@ -5330,7 +6058,7 @@ Execution failed: ${err instanceof Error ? err.message : String(err)}`));
5330
6058
  async function handleLogin(authClient, backendUrl) {
5331
6059
  const targetBackend = backendUrl || process.env.GROUPY_BACKEND_URL || "https://api.groupy-hub.store";
5332
6060
  console.log(style.brand(`
5333
- \uD83D\uDD10 Logging into Backend: ${targetBackend}`));
6061
+ Logging into Backend: ${targetBackend}`));
5334
6062
  try {
5335
6063
  const { authUrl, waitForToken } = await authClient.startOAuthFlow({
5336
6064
  backendUrl: targetBackend