@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/bin/pikaa.js +41 -30
- package/dist/cli.js +852 -112
- package/dist/index.js +54 -11
- package/package.json +1 -1
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 {
|
|
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
|
-
"##
|
|
458
|
-
"
|
|
459
|
-
"
|
|
460
|
-
"
|
|
461
|
-
"
|
|
462
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4086
|
-
CliFormatter.
|
|
4087
|
-
|
|
4088
|
-
|
|
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("
|
|
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("
|
|
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
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
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\
|
|
4219
|
-
for (let i = 0;i <
|
|
4220
|
-
const cmd =
|
|
4221
|
-
const
|
|
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
|
|
4224
|
-
const
|
|
4225
|
-
|
|
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
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4295
|
-
|
|
4296
|
-
const
|
|
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
|
-
|
|
4305
|
-
|
|
4306
|
-
const
|
|
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
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
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
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
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
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
if (
|
|
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
|
-
|
|
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 || "
|
|
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
|
-
|
|
5958
|
+
explicitBaseUrl = args[++i];
|
|
5959
|
+
baseUrl = explicitBaseUrl || baseUrl;
|
|
5225
5960
|
} else if (arg === "--api-key" || arg === "-k") {
|
|
5226
|
-
|
|
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
|
-
|
|
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:")}
|