codemaxxing 0.1.7 → 0.1.9
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/README.md +43 -0
- package/dist/agent.d.ts +31 -1
- package/dist/agent.js +357 -5
- package/dist/config.d.ts +3 -5
- package/dist/config.js +23 -0
- package/dist/index.js +208 -48
- package/dist/tools/files.d.ts +8 -0
- package/dist/tools/files.js +137 -0
- package/dist/utils/sessions.d.ts +7 -0
- package/dist/utils/sessions.js +26 -1
- package/package.json +2 -1
- package/src/agent.ts +380 -6
- package/src/config.ts +28 -1
- package/src/index.tsx +278 -53
- package/src/tools/files.ts +127 -0
- package/src/utils/sessions.ts +33 -1
package/src/index.tsx
CHANGED
|
@@ -6,13 +6,13 @@ import { EventEmitter } from "events";
|
|
|
6
6
|
import TextInput from "ink-text-input";
|
|
7
7
|
import { CodingAgent } from "./agent.js";
|
|
8
8
|
import { loadConfig, detectLocalProvider, parseCLIArgs, applyOverrides, listModels } from "./config.js";
|
|
9
|
-
import { listSessions, getSession, loadMessages } from "./utils/sessions.js";
|
|
9
|
+
import { listSessions, getSession, loadMessages, deleteSession } from "./utils/sessions.js";
|
|
10
10
|
import { execSync } from "child_process";
|
|
11
11
|
import { isGitRepo, getBranch, getStatus, getDiff, undoLastCommit } from "./utils/git.js";
|
|
12
12
|
import { getTheme, listThemes, THEMES, DEFAULT_THEME, type Theme } from "./themes.js";
|
|
13
13
|
import { PROVIDERS, getCredentials, openRouterOAuth, anthropicSetupToken, importCodexToken, importQwenToken, copilotDeviceFlow, saveApiKey } from "./utils/auth.js";
|
|
14
14
|
|
|
15
|
-
const VERSION = "0.1.
|
|
15
|
+
const VERSION = "0.1.9";
|
|
16
16
|
|
|
17
17
|
// ── Helpers ──
|
|
18
18
|
function formatTimeAgo(date: Date): string {
|
|
@@ -43,6 +43,7 @@ const SLASH_COMMANDS = [
|
|
|
43
43
|
{ cmd: "/theme", desc: "switch color theme" },
|
|
44
44
|
{ cmd: "/model", desc: "switch model mid-session" },
|
|
45
45
|
{ cmd: "/sessions", desc: "list past sessions" },
|
|
46
|
+
{ cmd: "/session delete", desc: "delete a session" },
|
|
46
47
|
{ cmd: "/resume", desc: "resume a past session" },
|
|
47
48
|
{ cmd: "/quit", desc: "exit" },
|
|
48
49
|
];
|
|
@@ -135,11 +136,17 @@ function App() {
|
|
|
135
136
|
const [sessionPickerIndex, setSessionPickerIndex] = useState(0);
|
|
136
137
|
const [themePicker, setThemePicker] = useState(false);
|
|
137
138
|
const [themePickerIndex, setThemePickerIndex] = useState(0);
|
|
139
|
+
const [deleteSessionPicker, setDeleteSessionPicker] = useState<Array<{ id: string; display: string }> | null>(null);
|
|
140
|
+
const [deleteSessionPickerIndex, setDeleteSessionPickerIndex] = useState(0);
|
|
141
|
+
const [deleteSessionConfirm, setDeleteSessionConfirm] = useState<{ id: string; display: string } | null>(null);
|
|
138
142
|
const [loginPicker, setLoginPicker] = useState(false);
|
|
139
143
|
const [loginPickerIndex, setLoginPickerIndex] = useState(0);
|
|
144
|
+
const [loginMethodPicker, setLoginMethodPicker] = useState<{ provider: string; methods: string[] } | null>(null);
|
|
145
|
+
const [loginMethodIndex, setLoginMethodIndex] = useState(0);
|
|
140
146
|
const [approval, setApproval] = useState<{
|
|
141
147
|
tool: string;
|
|
142
148
|
args: Record<string, unknown>;
|
|
149
|
+
diff?: string;
|
|
143
150
|
resolve: (decision: "yes" | "no" | "always") => void;
|
|
144
151
|
} | null>(null);
|
|
145
152
|
|
|
@@ -247,9 +254,15 @@ function App() {
|
|
|
247
254
|
onGitCommit: (message) => {
|
|
248
255
|
addMsg("info", `📝 Auto-committed: ${message}`);
|
|
249
256
|
},
|
|
250
|
-
|
|
257
|
+
onContextCompressed: (oldTokens, newTokens) => {
|
|
258
|
+
const saved = oldTokens - newTokens;
|
|
259
|
+
const savedStr = saved >= 1000 ? `${(saved / 1000).toFixed(1)}k` : String(saved);
|
|
260
|
+
addMsg("info", `📦 Context compressed (~${savedStr} tokens freed)`);
|
|
261
|
+
},
|
|
262
|
+
contextCompressionThreshold: config.defaults.contextCompressionThreshold,
|
|
263
|
+
onToolApproval: (name, args, diff) => {
|
|
251
264
|
return new Promise((resolve) => {
|
|
252
|
-
setApproval({ tool: name, args, resolve });
|
|
265
|
+
setApproval({ tool: name, args, diff, resolve });
|
|
253
266
|
setLoading(false);
|
|
254
267
|
});
|
|
255
268
|
},
|
|
@@ -296,7 +309,7 @@ function App() {
|
|
|
296
309
|
const selected = matches[idx];
|
|
297
310
|
if (selected) {
|
|
298
311
|
// Commands that need args (like /commit, /model) — fill input instead of executing
|
|
299
|
-
if (selected.cmd === "/commit" || selected.cmd === "/model") {
|
|
312
|
+
if (selected.cmd === "/commit" || selected.cmd === "/model" || selected.cmd === "/session delete") {
|
|
300
313
|
setInput(selected.cmd + " ");
|
|
301
314
|
setCmdIndex(0);
|
|
302
315
|
setInputKey((k) => k + 1);
|
|
@@ -341,6 +354,7 @@ function App() {
|
|
|
341
354
|
" /models — list available models",
|
|
342
355
|
" /map — show repository map",
|
|
343
356
|
" /sessions — list past sessions",
|
|
357
|
+
" /session delete — delete a session",
|
|
344
358
|
" /resume — resume a past session",
|
|
345
359
|
" /reset — clear conversation",
|
|
346
360
|
" /context — show message count",
|
|
@@ -451,12 +465,50 @@ function App() {
|
|
|
451
465
|
const tokens = s.token_estimate >= 1000
|
|
452
466
|
? `${(s.token_estimate / 1000).toFixed(1)}k`
|
|
453
467
|
: String(s.token_estimate);
|
|
454
|
-
|
|
468
|
+
const cost = s.estimated_cost > 0
|
|
469
|
+
? `$${s.estimated_cost < 0.01 ? s.estimated_cost.toFixed(4) : s.estimated_cost.toFixed(2)}`
|
|
470
|
+
: "";
|
|
471
|
+
return ` ${s.id} ${dir}/ ${s.message_count} msgs ~${tokens} tok${cost ? ` ${cost}` : ""} ${ago} ${s.model}`;
|
|
455
472
|
});
|
|
456
473
|
addMsg("info", "Recent sessions:\n" + lines.join("\n") + "\n\n Use /resume <id> to continue a session");
|
|
457
474
|
}
|
|
458
475
|
return;
|
|
459
476
|
}
|
|
477
|
+
if (trimmed.startsWith("/session delete")) {
|
|
478
|
+
const idArg = trimmed.replace("/session delete", "").trim();
|
|
479
|
+
if (idArg) {
|
|
480
|
+
// Direct delete by ID
|
|
481
|
+
const session = getSession(idArg);
|
|
482
|
+
if (!session) {
|
|
483
|
+
addMsg("error", `Session "${idArg}" not found.`);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const dir = session.cwd.split("/").pop() || session.cwd;
|
|
487
|
+
setDeleteSessionConfirm({ id: idArg, display: `${idArg} ${dir}/ ${session.message_count} msgs ${session.model}` });
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
// Show picker
|
|
491
|
+
const sessions = listSessions(10);
|
|
492
|
+
if (sessions.length === 0) {
|
|
493
|
+
addMsg("info", "No sessions to delete.");
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const items = sessions.map((s) => {
|
|
497
|
+
const date = new Date(s.updated_at + "Z");
|
|
498
|
+
const ago = formatTimeAgo(date);
|
|
499
|
+
const dir = s.cwd.split("/").pop() || s.cwd;
|
|
500
|
+
const tokens = s.token_estimate >= 1000
|
|
501
|
+
? `${(s.token_estimate / 1000).toFixed(1)}k`
|
|
502
|
+
: String(s.token_estimate);
|
|
503
|
+
return {
|
|
504
|
+
id: s.id,
|
|
505
|
+
display: `${s.id} ${dir}/ ${s.message_count} msgs ~${tokens} tok ${ago} ${s.model}`,
|
|
506
|
+
};
|
|
507
|
+
});
|
|
508
|
+
setDeleteSessionPicker(items);
|
|
509
|
+
setDeleteSessionPickerIndex(0);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
460
512
|
if (trimmed === "/resume") {
|
|
461
513
|
const sessions = listSessions(10);
|
|
462
514
|
if (sessions.length === 0) {
|
|
@@ -543,77 +595,115 @@ function App() {
|
|
|
543
595
|
}
|
|
544
596
|
}
|
|
545
597
|
|
|
546
|
-
// Login picker navigation
|
|
547
|
-
if (
|
|
548
|
-
const
|
|
598
|
+
// Login method picker navigation (second level — pick auth method)
|
|
599
|
+
if (loginMethodPicker) {
|
|
600
|
+
const methods = loginMethodPicker.methods;
|
|
549
601
|
if (key.upArrow) {
|
|
550
|
-
|
|
602
|
+
setLoginMethodIndex((prev: number) => (prev - 1 + methods.length) % methods.length);
|
|
551
603
|
return;
|
|
552
604
|
}
|
|
553
605
|
if (key.downArrow) {
|
|
554
|
-
|
|
606
|
+
setLoginMethodIndex((prev: number) => (prev + 1) % methods.length);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (key.escape) {
|
|
610
|
+
setLoginMethodPicker(null);
|
|
611
|
+
setLoginPicker(true); // go back to provider picker
|
|
555
612
|
return;
|
|
556
613
|
}
|
|
557
614
|
if (key.return) {
|
|
558
|
-
const
|
|
559
|
-
|
|
615
|
+
const method = methods[loginMethodIndex];
|
|
616
|
+
const providerId = loginMethodPicker.provider;
|
|
617
|
+
setLoginMethodPicker(null);
|
|
560
618
|
|
|
561
|
-
if (
|
|
619
|
+
if (method === "oauth" && providerId === "openrouter") {
|
|
562
620
|
addMsg("info", "Starting OpenRouter OAuth — opening browser...");
|
|
563
621
|
setLoading(true);
|
|
564
622
|
setSpinnerMsg("Waiting for authorization...");
|
|
565
623
|
openRouterOAuth((msg: string) => addMsg("info", msg))
|
|
566
|
-
.then((
|
|
567
|
-
addMsg("info", `✅ OpenRouter authenticated!
|
|
568
|
-
addMsg("info", `Switch with: /model anthropic/claude-sonnet-4`);
|
|
624
|
+
.then(() => {
|
|
625
|
+
addMsg("info", `✅ OpenRouter authenticated! Access to 200+ models.`);
|
|
569
626
|
setLoading(false);
|
|
570
627
|
})
|
|
571
|
-
.catch((err: any) => {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
});
|
|
575
|
-
} else if (selected.id === "anthropic") {
|
|
576
|
-
addMsg("info", "Starting Anthropic setup-token flow...");
|
|
628
|
+
.catch((err: any) => { addMsg("error", `OAuth failed: ${err.message}`); setLoading(false); });
|
|
629
|
+
} else if (method === "setup-token") {
|
|
630
|
+
addMsg("info", "Starting setup-token flow — browser will open...");
|
|
577
631
|
setLoading(true);
|
|
578
632
|
setSpinnerMsg("Waiting for Claude Code auth...");
|
|
579
633
|
anthropicSetupToken((msg: string) => addMsg("info", msg))
|
|
580
|
-
.then((cred) => {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
})
|
|
584
|
-
.catch((err: any) => {
|
|
585
|
-
addMsg("error", `Anthropic auth failed: ${err.message}`);
|
|
586
|
-
setLoading(false);
|
|
587
|
-
});
|
|
588
|
-
} else if (selected.id === "openai") {
|
|
634
|
+
.then((cred) => { addMsg("info", `✅ Anthropic authenticated! (${cred.label})`); setLoading(false); })
|
|
635
|
+
.catch((err: any) => { addMsg("error", `Auth failed: ${err.message}`); setLoading(false); });
|
|
636
|
+
} else if (method === "cached-token" && providerId === "openai") {
|
|
589
637
|
const imported = importCodexToken((msg: string) => addMsg("info", msg));
|
|
590
|
-
if (imported) {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
addMsg("info", "No Codex CLI found. Run: codemaxxing auth api-key openai <your-key>");
|
|
594
|
-
}
|
|
595
|
-
} else if (selected.id === "qwen") {
|
|
638
|
+
if (imported) { addMsg("info", `✅ Imported Codex credentials! (${imported.label})`); }
|
|
639
|
+
else { addMsg("info", "No Codex CLI found. Install Codex CLI and sign in first."); }
|
|
640
|
+
} else if (method === "cached-token" && providerId === "qwen") {
|
|
596
641
|
const imported = importQwenToken((msg: string) => addMsg("info", msg));
|
|
597
|
-
if (imported) {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
addMsg("info", "No Qwen CLI found. Run: codemaxxing auth api-key qwen <your-key>");
|
|
601
|
-
}
|
|
602
|
-
} else if (selected.id === "copilot") {
|
|
642
|
+
if (imported) { addMsg("info", `✅ Imported Qwen credentials! (${imported.label})`); }
|
|
643
|
+
else { addMsg("info", "No Qwen CLI found. Install Qwen CLI and sign in first."); }
|
|
644
|
+
} else if (method === "device-flow") {
|
|
603
645
|
addMsg("info", "Starting GitHub Copilot device flow...");
|
|
604
646
|
setLoading(true);
|
|
605
647
|
setSpinnerMsg("Waiting for GitHub authorization...");
|
|
606
648
|
copilotDeviceFlow((msg: string) => addMsg("info", msg))
|
|
607
|
-
.then(() => {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
649
|
+
.then(() => { addMsg("info", `✅ GitHub Copilot authenticated!`); setLoading(false); })
|
|
650
|
+
.catch((err: any) => { addMsg("error", `Copilot auth failed: ${err.message}`); setLoading(false); });
|
|
651
|
+
} else if (method === "api-key") {
|
|
652
|
+
const provider = PROVIDERS.find((p) => p.id === providerId);
|
|
653
|
+
addMsg("info", `Enter your API key via CLI:\n codemaxxing auth api-key ${providerId} <your-key>\n Get key at: ${provider?.consoleUrl ?? "your provider's dashboard"}`);
|
|
654
|
+
}
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Login picker navigation (first level — pick provider)
|
|
661
|
+
if (loginPicker) {
|
|
662
|
+
const loginProviders = PROVIDERS.filter((p) => p.id !== "local");
|
|
663
|
+
if (key.upArrow) {
|
|
664
|
+
setLoginPickerIndex((prev: number) => (prev - 1 + loginProviders.length) % loginProviders.length);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
if (key.downArrow) {
|
|
668
|
+
setLoginPickerIndex((prev: number) => (prev + 1) % loginProviders.length);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
if (key.return) {
|
|
672
|
+
const selected = loginProviders[loginPickerIndex];
|
|
673
|
+
setLoginPicker(false);
|
|
674
|
+
|
|
675
|
+
// Get available methods for this provider (filter out 'none')
|
|
676
|
+
const methods = selected.methods.filter((m) => m !== "none");
|
|
677
|
+
|
|
678
|
+
if (methods.length === 1) {
|
|
679
|
+
// Only one method — execute it directly
|
|
680
|
+
setLoginMethodPicker({ provider: selected.id, methods });
|
|
681
|
+
setLoginMethodIndex(0);
|
|
682
|
+
// Simulate Enter press on the single method
|
|
683
|
+
if (methods[0] === "oauth" && selected.id === "openrouter") {
|
|
684
|
+
setLoginMethodPicker(null);
|
|
685
|
+
addMsg("info", "Starting OpenRouter OAuth — opening browser...");
|
|
686
|
+
setLoading(true);
|
|
687
|
+
setSpinnerMsg("Waiting for authorization...");
|
|
688
|
+
openRouterOAuth((msg: string) => addMsg("info", msg))
|
|
689
|
+
.then(() => { addMsg("info", `✅ OpenRouter authenticated! Access to 200+ models.`); setLoading(false); })
|
|
690
|
+
.catch((err: any) => { addMsg("error", `OAuth failed: ${err.message}`); setLoading(false); });
|
|
691
|
+
} else if (methods[0] === "device-flow") {
|
|
692
|
+
setLoginMethodPicker(null);
|
|
693
|
+
addMsg("info", "Starting GitHub Copilot device flow...");
|
|
694
|
+
setLoading(true);
|
|
695
|
+
setSpinnerMsg("Waiting for GitHub authorization...");
|
|
696
|
+
copilotDeviceFlow((msg: string) => addMsg("info", msg))
|
|
697
|
+
.then(() => { addMsg("info", `✅ GitHub Copilot authenticated!`); setLoading(false); })
|
|
698
|
+
.catch((err: any) => { addMsg("error", `Copilot auth failed: ${err.message}`); setLoading(false); });
|
|
699
|
+
} else if (methods[0] === "api-key") {
|
|
700
|
+
setLoginMethodPicker(null);
|
|
701
|
+
addMsg("info", `Enter your API key via CLI:\n codemaxxing auth api-key ${selected.id} <your-key>\n Get key at: ${selected.consoleUrl ?? "your provider's dashboard"}`);
|
|
702
|
+
}
|
|
615
703
|
} else {
|
|
616
|
-
|
|
704
|
+
// Multiple methods — show submenu
|
|
705
|
+
setLoginMethodPicker({ provider: selected.id, methods });
|
|
706
|
+
setLoginMethodIndex(0);
|
|
617
707
|
}
|
|
618
708
|
return;
|
|
619
709
|
}
|
|
@@ -693,6 +783,54 @@ function App() {
|
|
|
693
783
|
return; // Ignore other keys during session picker
|
|
694
784
|
}
|
|
695
785
|
|
|
786
|
+
// Delete session confirmation (y/n)
|
|
787
|
+
if (deleteSessionConfirm) {
|
|
788
|
+
if (inputChar === "y" || inputChar === "Y") {
|
|
789
|
+
const deleted = deleteSession(deleteSessionConfirm.id);
|
|
790
|
+
if (deleted) {
|
|
791
|
+
addMsg("info", `✅ Deleted session ${deleteSessionConfirm.id}`);
|
|
792
|
+
} else {
|
|
793
|
+
addMsg("error", `Failed to delete session ${deleteSessionConfirm.id}`);
|
|
794
|
+
}
|
|
795
|
+
setDeleteSessionConfirm(null);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (inputChar === "n" || inputChar === "N" || key.escape) {
|
|
799
|
+
addMsg("info", "Delete cancelled.");
|
|
800
|
+
setDeleteSessionConfirm(null);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Delete session picker navigation
|
|
807
|
+
if (deleteSessionPicker) {
|
|
808
|
+
if (key.upArrow) {
|
|
809
|
+
setDeleteSessionPickerIndex((prev) => (prev - 1 + deleteSessionPicker.length) % deleteSessionPicker.length);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if (key.downArrow) {
|
|
813
|
+
setDeleteSessionPickerIndex((prev) => (prev + 1) % deleteSessionPicker.length);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if (key.return) {
|
|
817
|
+
const selected = deleteSessionPicker[deleteSessionPickerIndex];
|
|
818
|
+
if (selected) {
|
|
819
|
+
setDeleteSessionPicker(null);
|
|
820
|
+
setDeleteSessionPickerIndex(0);
|
|
821
|
+
setDeleteSessionConfirm(selected);
|
|
822
|
+
}
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
if (key.escape) {
|
|
826
|
+
setDeleteSessionPicker(null);
|
|
827
|
+
setDeleteSessionPickerIndex(0);
|
|
828
|
+
addMsg("info", "Delete cancelled.");
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
|
|
696
834
|
// Backspace with empty input → remove last paste chunk
|
|
697
835
|
if (key.backspace || key.delete) {
|
|
698
836
|
if (input === "" && pastedChunksRef.current.length > 0) {
|
|
@@ -844,6 +982,21 @@ function App() {
|
|
|
844
982
|
{approval.tool === "write_file" && approval.args.content ? (
|
|
845
983
|
<Text color={theme.colors.muted}>{" "}{String(approval.args.content).split("\n").length}{" lines, "}{String(approval.args.content).length}{"B"}</Text>
|
|
846
984
|
) : null}
|
|
985
|
+
{approval.diff ? (
|
|
986
|
+
<Box flexDirection="column" marginTop={0} marginLeft={2}>
|
|
987
|
+
{approval.diff.split("\n").slice(0, 40).map((line, i) => (
|
|
988
|
+
<Text key={i} color={
|
|
989
|
+
line.startsWith("+") ? theme.colors.success :
|
|
990
|
+
line.startsWith("-") ? theme.colors.error :
|
|
991
|
+
line.startsWith("@@") ? theme.colors.primary :
|
|
992
|
+
theme.colors.muted
|
|
993
|
+
}>{line}</Text>
|
|
994
|
+
))}
|
|
995
|
+
{approval.diff.split("\n").length > 40 ? (
|
|
996
|
+
<Text color={theme.colors.muted}>... ({approval.diff.split("\n").length - 40} more lines)</Text>
|
|
997
|
+
) : null}
|
|
998
|
+
</Box>
|
|
999
|
+
) : null}
|
|
847
1000
|
{approval.tool === "run_command" && approval.args.command ? (
|
|
848
1001
|
<Text color={theme.colors.muted}>{" $ "}{String(approval.args.command)}</Text>
|
|
849
1002
|
) : null}
|
|
@@ -855,6 +1008,45 @@ function App() {
|
|
|
855
1008
|
</Box>
|
|
856
1009
|
)}
|
|
857
1010
|
|
|
1011
|
+
{/* ═══ LOGIN PICKER ═══ */}
|
|
1012
|
+
{loginPicker && (
|
|
1013
|
+
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
|
|
1014
|
+
<Text bold color={theme.colors.secondary}>💪 Choose a provider:</Text>
|
|
1015
|
+
{PROVIDERS.filter((p) => p.id !== "local").map((p, i) => (
|
|
1016
|
+
<Text key={p.id}>
|
|
1017
|
+
{i === loginPickerIndex ? <Text color={theme.colors.suggestion} bold>{"▸ "}</Text> : <Text>{" "}</Text>}
|
|
1018
|
+
<Text color={i === loginPickerIndex ? theme.colors.suggestion : theme.colors.primary} bold>{p.name}</Text>
|
|
1019
|
+
<Text color={theme.colors.muted}>{" — "}{p.description}</Text>
|
|
1020
|
+
{getCredentials().some((c) => c.provider === p.id) ? <Text color={theme.colors.success}> ✓</Text> : null}
|
|
1021
|
+
</Text>
|
|
1022
|
+
))}
|
|
1023
|
+
<Text dimColor>{" ↑↓ navigate · Enter select · Esc cancel"}</Text>
|
|
1024
|
+
</Box>
|
|
1025
|
+
)}
|
|
1026
|
+
|
|
1027
|
+
{/* ═══ LOGIN METHOD PICKER ═══ */}
|
|
1028
|
+
{loginMethodPicker && (
|
|
1029
|
+
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
|
|
1030
|
+
<Text bold color={theme.colors.secondary}>How do you want to authenticate?</Text>
|
|
1031
|
+
{loginMethodPicker.methods.map((method, i) => {
|
|
1032
|
+
const labels: Record<string, string> = {
|
|
1033
|
+
"oauth": "🌐 Browser login (OAuth)",
|
|
1034
|
+
"setup-token": "🔑 Link subscription (via Claude Code CLI)",
|
|
1035
|
+
"cached-token": "📦 Import from existing CLI",
|
|
1036
|
+
"api-key": "🔒 Enter API key manually",
|
|
1037
|
+
"device-flow": "📱 Device flow (GitHub)",
|
|
1038
|
+
};
|
|
1039
|
+
return (
|
|
1040
|
+
<Text key={method}>
|
|
1041
|
+
{i === loginMethodIndex ? <Text color={theme.colors.suggestion} bold>{"▸ "}</Text> : <Text>{" "}</Text>}
|
|
1042
|
+
<Text color={i === loginMethodIndex ? theme.colors.suggestion : theme.colors.primary} bold>{labels[method] ?? method}</Text>
|
|
1043
|
+
</Text>
|
|
1044
|
+
);
|
|
1045
|
+
})}
|
|
1046
|
+
<Text dimColor>{" ↑↓ navigate · Enter select · Esc back"}</Text>
|
|
1047
|
+
</Box>
|
|
1048
|
+
)}
|
|
1049
|
+
|
|
858
1050
|
{/* ═══ THEME PICKER ═══ */}
|
|
859
1051
|
{themePicker && (
|
|
860
1052
|
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
|
|
@@ -885,6 +1077,32 @@ function App() {
|
|
|
885
1077
|
</Box>
|
|
886
1078
|
)}
|
|
887
1079
|
|
|
1080
|
+
{/* ═══ DELETE SESSION PICKER ═══ */}
|
|
1081
|
+
{deleteSessionPicker && (
|
|
1082
|
+
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.error} paddingX={1} marginBottom={0}>
|
|
1083
|
+
<Text bold color={theme.colors.error}>Delete a session:</Text>
|
|
1084
|
+
{deleteSessionPicker.map((s, i) => (
|
|
1085
|
+
<Text key={s.id}>
|
|
1086
|
+
{i === deleteSessionPickerIndex ? <Text color={theme.colors.suggestion} bold>{"▸ "}</Text> : <Text>{" "}</Text>}
|
|
1087
|
+
<Text color={i === deleteSessionPickerIndex ? theme.colors.suggestion : theme.colors.muted}>{s.display}</Text>
|
|
1088
|
+
</Text>
|
|
1089
|
+
))}
|
|
1090
|
+
<Text dimColor>{" ↑↓ navigate · Enter select · Esc cancel"}</Text>
|
|
1091
|
+
</Box>
|
|
1092
|
+
)}
|
|
1093
|
+
|
|
1094
|
+
{/* ═══ DELETE SESSION CONFIRM ═══ */}
|
|
1095
|
+
{deleteSessionConfirm && (
|
|
1096
|
+
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.warning} paddingX={1} marginBottom={0}>
|
|
1097
|
+
<Text bold color={theme.colors.warning}>Delete session {deleteSessionConfirm.id}?</Text>
|
|
1098
|
+
<Text color={theme.colors.muted}>{" "}{deleteSessionConfirm.display}</Text>
|
|
1099
|
+
<Text>
|
|
1100
|
+
<Text color={theme.colors.error} bold> [y]</Text><Text>es </Text>
|
|
1101
|
+
<Text color={theme.colors.success} bold>[n]</Text><Text>o</Text>
|
|
1102
|
+
</Text>
|
|
1103
|
+
</Box>
|
|
1104
|
+
)}
|
|
1105
|
+
|
|
888
1106
|
{/* ═══ COMMAND SUGGESTIONS ═══ */}
|
|
889
1107
|
{showSuggestions && (
|
|
890
1108
|
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.muted} paddingX={1} marginBottom={0}>
|
|
@@ -931,6 +1149,13 @@ function App() {
|
|
|
931
1149
|
return tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : String(tokens);
|
|
932
1150
|
})()}
|
|
933
1151
|
{" tokens"}
|
|
1152
|
+
{(() => {
|
|
1153
|
+
const { totalCost } = agent.getCostInfo();
|
|
1154
|
+
if (totalCost > 0) {
|
|
1155
|
+
return ` · 💰 $${totalCost < 0.01 ? totalCost.toFixed(4) : totalCost.toFixed(2)}`;
|
|
1156
|
+
}
|
|
1157
|
+
return "";
|
|
1158
|
+
})()}
|
|
934
1159
|
{modelName ? ` · 🤖 ${modelName}` : ""}
|
|
935
1160
|
</Text>
|
|
936
1161
|
</Box>
|
package/src/tools/files.ts
CHANGED
|
@@ -177,6 +177,133 @@ export async function executeTool(
|
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Generate a simple unified diff between two strings
|
|
182
|
+
*/
|
|
183
|
+
export function generateDiff(oldContent: string, newContent: string, filePath: string): string {
|
|
184
|
+
const oldLines = oldContent.split("\n");
|
|
185
|
+
const newLines = newContent.split("\n");
|
|
186
|
+
const output: string[] = [`--- a/${filePath}`, `+++ b/${filePath}`];
|
|
187
|
+
|
|
188
|
+
const lcs = computeLCS(oldLines, newLines);
|
|
189
|
+
|
|
190
|
+
let oi = 0, ni = 0, li = 0;
|
|
191
|
+
let hunkLines: string[] = [];
|
|
192
|
+
let hunkOldCount = 0;
|
|
193
|
+
let hunkNewCount = 0;
|
|
194
|
+
let hunkStartOld = 1;
|
|
195
|
+
let hunkStartNew = 1;
|
|
196
|
+
let pendingContext: string[] = [];
|
|
197
|
+
let hasHunk = false;
|
|
198
|
+
|
|
199
|
+
function flushHunk() {
|
|
200
|
+
if (hasHunk && hunkLines.length > 0) {
|
|
201
|
+
output.push(`@@ -${hunkStartOld},${hunkOldCount} +${hunkStartNew},${hunkNewCount} @@`);
|
|
202
|
+
output.push(...hunkLines);
|
|
203
|
+
}
|
|
204
|
+
hunkLines = [];
|
|
205
|
+
hunkOldCount = 0;
|
|
206
|
+
hunkNewCount = 0;
|
|
207
|
+
hasHunk = false;
|
|
208
|
+
pendingContext = [];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function startHunk() {
|
|
212
|
+
if (!hasHunk) {
|
|
213
|
+
hasHunk = true;
|
|
214
|
+
hunkStartOld = Math.max(1, oi + 1 - 3);
|
|
215
|
+
hunkStartNew = Math.max(1, ni + 1 - 3);
|
|
216
|
+
const contextStart = Math.max(0, oi - 3);
|
|
217
|
+
for (let c = contextStart; c < oi; c++) {
|
|
218
|
+
hunkLines.push(` ${oldLines[c]}`);
|
|
219
|
+
hunkOldCount++;
|
|
220
|
+
hunkNewCount++;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (pendingContext.length > 0) {
|
|
224
|
+
hunkLines.push(...pendingContext);
|
|
225
|
+
pendingContext = [];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
while (oi < oldLines.length || ni < newLines.length) {
|
|
230
|
+
if (li < lcs.length && oi < oldLines.length && ni < newLines.length &&
|
|
231
|
+
oldLines[oi] === lcs[li] && newLines[ni] === lcs[li]) {
|
|
232
|
+
// Matching line
|
|
233
|
+
if (hasHunk) {
|
|
234
|
+
pendingContext.push(` ${oldLines[oi]}`);
|
|
235
|
+
hunkOldCount++;
|
|
236
|
+
hunkNewCount++;
|
|
237
|
+
if (pendingContext.length > 6) flushHunk();
|
|
238
|
+
}
|
|
239
|
+
oi++; ni++; li++;
|
|
240
|
+
} else if (oi < oldLines.length && (li >= lcs.length || oldLines[oi] !== lcs[li])) {
|
|
241
|
+
startHunk();
|
|
242
|
+
hunkLines.push(`-${oldLines[oi]}`);
|
|
243
|
+
hunkOldCount++;
|
|
244
|
+
oi++;
|
|
245
|
+
} else if (ni < newLines.length && (li >= lcs.length || newLines[ni] !== lcs[li])) {
|
|
246
|
+
startHunk();
|
|
247
|
+
hunkLines.push(`+${newLines[ni]}`);
|
|
248
|
+
hunkNewCount++;
|
|
249
|
+
ni++;
|
|
250
|
+
} else {
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
flushHunk();
|
|
256
|
+
|
|
257
|
+
if (output.length <= 2) return "(no changes)";
|
|
258
|
+
|
|
259
|
+
const maxDiffLines = 60;
|
|
260
|
+
if (output.length > maxDiffLines + 2) {
|
|
261
|
+
return output.slice(0, maxDiffLines + 2).join("\n") + `\n... (${output.length - maxDiffLines - 2} more lines)`;
|
|
262
|
+
}
|
|
263
|
+
return output.join("\n");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function computeLCS(a: string[], b: string[]): string[] {
|
|
267
|
+
if (a.length > 500 || b.length > 500) {
|
|
268
|
+
// For large files, just return common lines in order
|
|
269
|
+
const result: string[] = [];
|
|
270
|
+
let bi = 0;
|
|
271
|
+
for (const line of a) {
|
|
272
|
+
while (bi < b.length && b[bi] !== line) bi++;
|
|
273
|
+
if (bi < b.length) { result.push(line); bi++; }
|
|
274
|
+
}
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
const m = a.length, n = b.length;
|
|
278
|
+
const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
279
|
+
for (let i = 1; i <= m; i++) {
|
|
280
|
+
for (let j = 1; j <= n; j++) {
|
|
281
|
+
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const result: string[] = [];
|
|
285
|
+
let i = m, j = n;
|
|
286
|
+
while (i > 0 && j > 0) {
|
|
287
|
+
if (a[i - 1] === b[j - 1]) { result.unshift(a[i - 1]); i--; j--; }
|
|
288
|
+
else if (dp[i - 1][j] > dp[i][j - 1]) { i--; }
|
|
289
|
+
else { j--; }
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Get existing file content for diff preview (returns null if file doesn't exist)
|
|
296
|
+
*/
|
|
297
|
+
export function getExistingContent(filePath: string, cwd: string): string | null {
|
|
298
|
+
const fullPath = join(cwd, filePath);
|
|
299
|
+
if (!existsSync(fullPath)) return null;
|
|
300
|
+
try {
|
|
301
|
+
return readFileSync(fullPath, "utf-8");
|
|
302
|
+
} catch {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
180
307
|
function listDir(
|
|
181
308
|
dirPath: string,
|
|
182
309
|
cwd: string,
|
package/src/utils/sessions.ts
CHANGED
|
@@ -34,7 +34,10 @@ function getDb(): Database.Database {
|
|
|
34
34
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
35
35
|
message_count INTEGER NOT NULL DEFAULT 0,
|
|
36
36
|
token_estimate INTEGER NOT NULL DEFAULT 0,
|
|
37
|
-
summary TEXT
|
|
37
|
+
summary TEXT,
|
|
38
|
+
prompt_tokens INTEGER NOT NULL DEFAULT 0,
|
|
39
|
+
completion_tokens INTEGER NOT NULL DEFAULT 0,
|
|
40
|
+
estimated_cost REAL NOT NULL DEFAULT 0
|
|
38
41
|
);
|
|
39
42
|
|
|
40
43
|
CREATE TABLE IF NOT EXISTS messages (
|
|
@@ -51,6 +54,17 @@ function getDb(): Database.Database {
|
|
|
51
54
|
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
|
|
52
55
|
`);
|
|
53
56
|
|
|
57
|
+
// Migrate: add cost columns if missing (for existing DBs)
|
|
58
|
+
try {
|
|
59
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN prompt_tokens INTEGER NOT NULL DEFAULT 0`);
|
|
60
|
+
} catch { /* column already exists */ }
|
|
61
|
+
try {
|
|
62
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN completion_tokens INTEGER NOT NULL DEFAULT 0`);
|
|
63
|
+
} catch { /* column already exists */ }
|
|
64
|
+
try {
|
|
65
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN estimated_cost REAL NOT NULL DEFAULT 0`);
|
|
66
|
+
} catch { /* column already exists */ }
|
|
67
|
+
|
|
54
68
|
return db;
|
|
55
69
|
}
|
|
56
70
|
|
|
@@ -129,6 +143,9 @@ export interface SessionInfo {
|
|
|
129
143
|
message_count: number;
|
|
130
144
|
token_estimate: number;
|
|
131
145
|
summary: string | null;
|
|
146
|
+
prompt_tokens: number;
|
|
147
|
+
completion_tokens: number;
|
|
148
|
+
estimated_cost: number;
|
|
132
149
|
}
|
|
133
150
|
|
|
134
151
|
export function listSessions(limit: number = 10): SessionInfo[] {
|
|
@@ -191,6 +208,21 @@ export function deleteSession(sessionId: string): boolean {
|
|
|
191
208
|
return result.changes > 0;
|
|
192
209
|
}
|
|
193
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Update cost tracking for a session
|
|
213
|
+
*/
|
|
214
|
+
export function updateSessionCost(
|
|
215
|
+
sessionId: string,
|
|
216
|
+
promptTokens: number,
|
|
217
|
+
completionTokens: number,
|
|
218
|
+
estimatedCost: number
|
|
219
|
+
): void {
|
|
220
|
+
const db = getDb();
|
|
221
|
+
db.prepare(`
|
|
222
|
+
UPDATE sessions SET prompt_tokens = ?, completion_tokens = ?, estimated_cost = ? WHERE id = ?
|
|
223
|
+
`).run(promptTokens, completionTokens, estimatedCost, sessionId);
|
|
224
|
+
}
|
|
225
|
+
|
|
194
226
|
/**
|
|
195
227
|
* Update session summary (for context compression)
|
|
196
228
|
*/
|