@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/bin/pikaa.js +41 -31
- package/dist/cli.js +829 -101
- package/dist/index.js +43 -4
- 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;
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
4086
|
-
CliFormatter.
|
|
4087
|
-
|
|
4088
|
-
|
|
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("
|
|
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("
|
|
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
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
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\
|
|
4219
|
-
for (let i = 0;i <
|
|
4220
|
-
const cmd =
|
|
4221
|
-
const
|
|
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
|
|
4224
|
-
const
|
|
4225
|
-
|
|
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
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4295
|
-
|
|
4296
|
-
const
|
|
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
|
-
|
|
4305
|
-
|
|
4306
|
-
const
|
|
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
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
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
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
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
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
6061
|
+
Logging into Backend: ${targetBackend}`));
|
|
5334
6062
|
try {
|
|
5335
6063
|
const { authUrl, waitForToken } = await authClient.startOAuthFlow({
|
|
5336
6064
|
backendUrl: targetBackend
|