@ridit/lens 0.2.2 → 0.2.5
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/LENS.md +18 -11
- package/README.md +91 -0
- package/dist/index.mjs +69273 -2244
- package/package.json +7 -3
- package/src/commands/commit.tsx +713 -0
- package/src/components/chat/ChatOverlays.tsx +14 -4
- package/src/components/chat/ChatRunner.tsx +197 -30
- package/src/components/timeline/CommitDetail.tsx +2 -4
- package/src/components/timeline/CommitList.tsx +2 -14
- package/src/components/timeline/TimelineChat.tsx +1 -2
- package/src/components/timeline/TimelineRunner.tsx +505 -422
- package/src/index.tsx +41 -0
- package/src/prompts/fewshot.ts +100 -3
- package/src/prompts/system.ts +16 -20
- package/src/tools/chart.ts +210 -0
- package/src/tools/convert-image.ts +312 -0
- package/src/tools/files.ts +1 -9
- package/src/tools/git.ts +577 -0
- package/src/tools/index.ts +17 -13
- package/src/tools/view-image.ts +335 -0
- package/src/tools/web.ts +0 -4
- package/src/utils/addons/loadAddons.ts +29 -0
- package/src/utils/chat.ts +8 -18
- package/src/utils/memory.ts +7 -12
- package/src/utils/thinking.tsx +275 -162
- package/src/utils/tools/builtins.ts +9 -32
- package/src/utils/tools/registry.ts +3 -59
- package/hello.py +0 -51
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, { useState, useEffect } from "react";
|
|
2
|
-
import { Box, Text, Static, useInput } from "ink";
|
|
1
|
+
import React, { useState, useEffect, useRef } from "react";
|
|
2
|
+
import { Box, Text, Static, useInput, useStdout } from "ink";
|
|
3
3
|
import TextInput from "ink-text-input";
|
|
4
4
|
import { execSync } from "child_process";
|
|
5
5
|
import { ProviderPicker } from "../repo/ProviderPicker";
|
|
@@ -9,14 +9,18 @@ import {
|
|
|
9
9
|
isGitRepo,
|
|
10
10
|
summarizeTimeline,
|
|
11
11
|
} from "../../utils/git";
|
|
12
|
-
import { callChat } from "../../utils/chat";
|
|
12
|
+
import { callChat, parseResponse } from "../../utils/chat";
|
|
13
|
+
import { registry } from "../../utils/tools/registry";
|
|
14
|
+
import { buildGitToolsPromptSection } from "../../tools/git";
|
|
13
15
|
import type { Commit, DiffFile } from "../../utils/git";
|
|
14
16
|
import type { Provider } from "../../types/config";
|
|
17
|
+
import type { Message } from "../../types/chat";
|
|
18
|
+
import { TypewriterText, InputBox } from "../chat/ChatOverlays";
|
|
19
|
+
import { ACCENT } from "../../colors";
|
|
15
20
|
|
|
16
|
-
const ACCENT = "#FF8C00";
|
|
17
21
|
const W = () => process.stdout.columns ?? 100;
|
|
18
22
|
|
|
19
|
-
// ── git
|
|
23
|
+
// ── git runner (only used by RevertConfirm) ───────────────────────────────────
|
|
20
24
|
|
|
21
25
|
function gitRun(cmd: string, cwd: string): { ok: boolean; out: string } {
|
|
22
26
|
try {
|
|
@@ -34,59 +38,30 @@ function gitRun(cmd: string, cwd: string): { ok: boolean; out: string } {
|
|
|
34
38
|
}
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
})
|
|
63
|
-
.join("\n\n");
|
|
64
|
-
|
|
65
|
-
return [tracked.slice(0, 4000), untrackedContent]
|
|
66
|
-
.filter(Boolean)
|
|
67
|
-
.join("\n\n");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function generateCommitMessage(
|
|
71
|
-
provider: Provider,
|
|
72
|
-
diff: string,
|
|
73
|
-
): Promise<string> {
|
|
74
|
-
const system = `You are a commit message generator. Given a git diff, write a concise, imperative commit message.
|
|
75
|
-
Rules:
|
|
76
|
-
- First line: short summary, max 72 chars, imperative mood ("add", "fix", "update", not "added")
|
|
77
|
-
- If needed, one blank line then a short body (2-3 lines max)
|
|
78
|
-
- No markdown, no bullet points, no code blocks
|
|
79
|
-
- Output ONLY the commit message, nothing else`;
|
|
80
|
-
|
|
81
|
-
const msgs = [
|
|
82
|
-
{
|
|
83
|
-
role: "user" as const,
|
|
84
|
-
content: `Write a commit message for this diff:\n\n${diff}`,
|
|
85
|
-
type: "text" as const,
|
|
86
|
-
},
|
|
87
|
-
];
|
|
88
|
-
const raw = await callChat(provider, system, msgs as any);
|
|
89
|
-
return typeof raw === "string" ? raw.trim() : "update files";
|
|
41
|
+
// ── thinking phrases ──────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
const THINKING_PHRASES = [
|
|
44
|
+
"thinking…",
|
|
45
|
+
"reading the repo…",
|
|
46
|
+
"consulting the log…",
|
|
47
|
+
"grepping the history…",
|
|
48
|
+
"diffing the vibes…",
|
|
49
|
+
"sniffing the diff...",
|
|
50
|
+
"reading your crimes...",
|
|
51
|
+
"crafting the perfect commit message...",
|
|
52
|
+
"pretending this was intentional all along...",
|
|
53
|
+
"making it sound like a feature...",
|
|
54
|
+
"turning chaos into conventional commits...",
|
|
55
|
+
"72 chars or bust...",
|
|
56
|
+
"git log will remember this...",
|
|
57
|
+
"committing to the bit. and also the repo...",
|
|
58
|
+
"staging your changes (and your career)...",
|
|
59
|
+
"making main proud...",
|
|
60
|
+
"git blame: not it...",
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
function randomPhrase() {
|
|
64
|
+
return THINKING_PHRASES[Math.floor(Math.random() * THINKING_PHRASES.length)]!;
|
|
90
65
|
}
|
|
91
66
|
|
|
92
67
|
// ── tiny helpers ──────────────────────────────────────────────────────────────
|
|
@@ -387,7 +362,6 @@ function RevertConfirm({
|
|
|
387
362
|
if (status !== "confirm") return;
|
|
388
363
|
if (input === "y" || input === "Y" || key.return) {
|
|
389
364
|
setStatus("running");
|
|
390
|
-
// use revert (safe — creates a new commit, doesn't rewrite history)
|
|
391
365
|
const r = gitRun(`git revert --no-edit "${commit.hash}"`, repoPath);
|
|
392
366
|
setResult(r.out);
|
|
393
367
|
setStatus("done");
|
|
@@ -455,341 +429,407 @@ function RevertConfirm({
|
|
|
455
429
|
);
|
|
456
430
|
}
|
|
457
431
|
|
|
458
|
-
// ──
|
|
432
|
+
// ── MsgBody ───────────────────────────────────────────────────────────────────
|
|
433
|
+
// Mirrors MessageBody from ChatMessage.tsx — inline code, bold, lists, code blocks.
|
|
459
434
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
435
|
+
function InlineText({ text }: { text: string }) {
|
|
436
|
+
const parts = text.split(/(`[^`]+`|\*\*[^*]+\*\*)/g);
|
|
437
|
+
return (
|
|
438
|
+
<>
|
|
439
|
+
{parts.map((part, i) => {
|
|
440
|
+
if (part.startsWith("`") && part.endsWith("`"))
|
|
441
|
+
return (
|
|
442
|
+
<Text key={i} color={ACCENT}>
|
|
443
|
+
{part.slice(1, -1)}
|
|
444
|
+
</Text>
|
|
445
|
+
);
|
|
446
|
+
if (part.startsWith("**") && part.endsWith("**"))
|
|
447
|
+
return (
|
|
448
|
+
<Text key={i} bold color="white">
|
|
449
|
+
{part.slice(2, -2)}
|
|
450
|
+
</Text>
|
|
451
|
+
);
|
|
452
|
+
return (
|
|
453
|
+
<Text key={i} color="white">
|
|
454
|
+
{part}
|
|
455
|
+
</Text>
|
|
456
|
+
);
|
|
457
|
+
})}
|
|
458
|
+
</>
|
|
459
|
+
);
|
|
460
|
+
}
|
|
469
461
|
|
|
470
|
-
function
|
|
462
|
+
function MsgBody({ content }: { content: string }) {
|
|
463
|
+
const segments = content.split(/(```[\s\S]*?```)/g);
|
|
464
|
+
return (
|
|
465
|
+
<Box flexDirection="column">
|
|
466
|
+
{segments.map((seg, si) => {
|
|
467
|
+
if (seg.startsWith("```")) {
|
|
468
|
+
const lines = seg.slice(3).split("\n");
|
|
469
|
+
const code = lines
|
|
470
|
+
.slice(1)
|
|
471
|
+
.join("\n")
|
|
472
|
+
.replace(/```\s*$/, "")
|
|
473
|
+
.trimEnd();
|
|
474
|
+
return (
|
|
475
|
+
<Box key={si} flexDirection="column">
|
|
476
|
+
{code.split("\n").map((line, li) => (
|
|
477
|
+
<Text key={li} color={ACCENT}>
|
|
478
|
+
{" "}
|
|
479
|
+
{line}
|
|
480
|
+
</Text>
|
|
481
|
+
))}
|
|
482
|
+
</Box>
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
const lines = seg.split("\n").filter((l) => l.trim() !== "");
|
|
486
|
+
return (
|
|
487
|
+
<Box key={si} flexDirection="column">
|
|
488
|
+
{lines.map((line, li) => {
|
|
489
|
+
if (line.match(/^[-*•]\s/))
|
|
490
|
+
return (
|
|
491
|
+
<Box key={li} gap={1}>
|
|
492
|
+
<Text color={ACCENT}>*</Text>
|
|
493
|
+
<InlineText text={line.slice(2).trim()} />
|
|
494
|
+
</Box>
|
|
495
|
+
);
|
|
496
|
+
if (line.match(/^\d+\.\s/)) {
|
|
497
|
+
const num = line.match(/^(\d+)\.\s/)![1];
|
|
498
|
+
return (
|
|
499
|
+
<Box key={li} gap={1}>
|
|
500
|
+
<Text color="gray">{num}.</Text>
|
|
501
|
+
<InlineText text={line.replace(/^\d+\.\s/, "").trim()} />
|
|
502
|
+
</Box>
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
return (
|
|
506
|
+
<Box key={li}>
|
|
507
|
+
<InlineText text={line} />
|
|
508
|
+
</Box>
|
|
509
|
+
);
|
|
510
|
+
})}
|
|
511
|
+
</Box>
|
|
512
|
+
);
|
|
513
|
+
})}
|
|
514
|
+
</Box>
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ── AskPanel ──────────────────────────────────────────────────────────────────
|
|
519
|
+
//
|
|
520
|
+
// Uses the global registry + parseResponse — identical execution path to
|
|
521
|
+
// ChatRunner.processResponse. Git tools come from tools/git.ts which registers
|
|
522
|
+
// them into the registry at startup. No local tool definitions here.
|
|
523
|
+
|
|
524
|
+
type AskMsg =
|
|
525
|
+
| { kind: "user"; content: string }
|
|
526
|
+
| { kind: "assistant"; content: string }
|
|
527
|
+
| { kind: "thinking" }
|
|
528
|
+
| { kind: "image"; ansi: string }
|
|
529
|
+
| {
|
|
530
|
+
kind: "tool";
|
|
531
|
+
toolName: string;
|
|
532
|
+
label: string;
|
|
533
|
+
result?: string;
|
|
534
|
+
approved?: boolean;
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
type PendingTool = {
|
|
538
|
+
toolName: string;
|
|
539
|
+
input: unknown;
|
|
540
|
+
rawInput: string;
|
|
541
|
+
remainder: string | undefined;
|
|
542
|
+
history: Message[];
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
function AskPanel({
|
|
546
|
+
commits,
|
|
471
547
|
repoPath,
|
|
472
548
|
provider,
|
|
473
|
-
|
|
549
|
+
onReload,
|
|
474
550
|
}: {
|
|
551
|
+
commits: Commit[];
|
|
475
552
|
repoPath: string;
|
|
476
553
|
provider: Provider;
|
|
477
|
-
|
|
554
|
+
onReload: () => void;
|
|
478
555
|
}) {
|
|
479
|
-
const [
|
|
556
|
+
const [messages, setMessages] = useState<AskMsg[]>([]);
|
|
557
|
+
const [apiHistory, setApiHistory] = useState<Message[]>([]);
|
|
558
|
+
const [input, setInput] = useState("");
|
|
559
|
+
const [thinking, setThinking] = useState(false);
|
|
560
|
+
const [phrase, setPhrase] = useState(randomPhrase);
|
|
561
|
+
const [pending, setPending] = useState<PendingTool | null>(null);
|
|
562
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
563
|
+
const { stdout } = useStdout();
|
|
480
564
|
|
|
481
|
-
//
|
|
565
|
+
// Rotate thinking phrase while busy
|
|
482
566
|
useEffect(() => {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
567
|
+
if (!thinking) return;
|
|
568
|
+
setPhrase(randomPhrase());
|
|
569
|
+
const id = setInterval(() => setPhrase(randomPhrase()), 3200);
|
|
570
|
+
return () => clearInterval(id);
|
|
571
|
+
}, [thinking]);
|
|
572
|
+
|
|
573
|
+
const systemPrompt = `You are a git assistant embedded in a terminal timeline viewer.
|
|
574
|
+
Repository: ${repoPath}
|
|
575
|
+
|
|
576
|
+
You have access to git tools to answer questions and perform git operations.
|
|
577
|
+
${buildGitToolsPromptSection()}
|
|
578
|
+
|
|
579
|
+
Rules:
|
|
580
|
+
- Use read tools freely to answer questions requiring live data
|
|
581
|
+
- For write operations briefly explain what you are about to do before emitting the tag
|
|
582
|
+
- After a tool result is returned, continue your response naturally
|
|
583
|
+
- Plain text only — no markdown headers
|
|
584
|
+
- Be concise
|
|
585
|
+
|
|
586
|
+
Timeline summary (last 300 commits):
|
|
587
|
+
${summarizeTimeline(commits)}`;
|
|
588
|
+
|
|
589
|
+
// ── core process loop — mirrors ChatRunner.processResponse ─────────────────
|
|
590
|
+
|
|
591
|
+
const processResponse = (
|
|
592
|
+
raw: string,
|
|
593
|
+
currentHistory: Message[],
|
|
594
|
+
signal: AbortSignal,
|
|
595
|
+
) => {
|
|
596
|
+
if (signal.aborted) {
|
|
597
|
+
setThinking(false);
|
|
486
598
|
return;
|
|
487
599
|
}
|
|
488
|
-
setState({ phase: "generating", diff });
|
|
489
|
-
generateCommitMessage(provider, diff)
|
|
490
|
-
.then((msg) => setState({ phase: "review", diff, message: msg }))
|
|
491
|
-
.catch((e) => setState({ phase: "error", message: String(e) }));
|
|
492
|
-
}, []);
|
|
493
600
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
601
|
+
const parsed = parseResponse(raw);
|
|
602
|
+
|
|
603
|
+
// plain text
|
|
604
|
+
if (parsed.kind === "text") {
|
|
605
|
+
const clean = parsed.content.replace(/\*\*([^*]+)\*\*/g, "$1").trim();
|
|
606
|
+
setMessages((prev) => [
|
|
607
|
+
...prev.filter((m) => m.kind !== "thinking"),
|
|
608
|
+
{ kind: "assistant", content: clean },
|
|
609
|
+
]);
|
|
610
|
+
setApiHistory([
|
|
611
|
+
...currentHistory,
|
|
612
|
+
{ role: "assistant", content: clean, type: "text" },
|
|
613
|
+
]);
|
|
614
|
+
setThinking(false);
|
|
501
615
|
return;
|
|
502
616
|
}
|
|
503
617
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
if (!add.ok) {
|
|
510
|
-
setState({ phase: "error", message: add.out });
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
const commit = gitRun(
|
|
514
|
-
`git commit -m ${JSON.stringify(state.message)}`,
|
|
515
|
-
repoPath,
|
|
516
|
-
);
|
|
517
|
-
setState({
|
|
518
|
-
phase: "done",
|
|
519
|
-
result: commit.ok ? commit.out : `Error: ${commit.out}`,
|
|
520
|
-
});
|
|
521
|
-
setTimeout(() => onDone(commit.ok ? state.message : null), 1500);
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
if (input === "e" || input === "E") {
|
|
525
|
-
setState({
|
|
526
|
-
phase: "editing",
|
|
527
|
-
diff: state.diff,
|
|
528
|
-
message: state.message,
|
|
529
|
-
});
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
if (input === "n" || input === "N" || key.escape) {
|
|
533
|
-
onDone(null);
|
|
618
|
+
// tool call
|
|
619
|
+
if (parsed.kind === "tool") {
|
|
620
|
+
const tool = registry.get(parsed.toolName);
|
|
621
|
+
if (!tool) {
|
|
622
|
+
setThinking(false);
|
|
534
623
|
return;
|
|
535
624
|
}
|
|
536
|
-
}
|
|
537
625
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
626
|
+
const label = tool.summariseInput
|
|
627
|
+
? String(tool.summariseInput(parsed.input))
|
|
628
|
+
: parsed.rawInput;
|
|
629
|
+
|
|
630
|
+
if (tool.safe) {
|
|
631
|
+
// Auto-approve — keep thinking true the whole time so input stays locked.
|
|
632
|
+
// Replace the thinking bubble with preamble (if any) + tool row + new thinking bubble.
|
|
633
|
+
setMessages((prev) => [
|
|
634
|
+
...prev.filter((m) => m.kind !== "thinking"),
|
|
635
|
+
...(parsed.content
|
|
636
|
+
? [{ kind: "assistant" as const, content: parsed.content }]
|
|
637
|
+
: []),
|
|
638
|
+
{
|
|
639
|
+
kind: "tool" as const,
|
|
640
|
+
toolName: parsed.toolName,
|
|
641
|
+
label,
|
|
642
|
+
approved: true,
|
|
643
|
+
},
|
|
644
|
+
{ kind: "thinking" as const },
|
|
645
|
+
]);
|
|
646
|
+
executeAndContinue(
|
|
647
|
+
{
|
|
648
|
+
toolName: parsed.toolName,
|
|
649
|
+
input: parsed.input,
|
|
650
|
+
rawInput: parsed.rawInput,
|
|
651
|
+
remainder: parsed.remainder,
|
|
652
|
+
history: currentHistory,
|
|
653
|
+
},
|
|
654
|
+
true,
|
|
655
|
+
signal,
|
|
656
|
+
);
|
|
657
|
+
} else {
|
|
658
|
+
// Write tool — stop thinking, show permission prompt, block input via pending.
|
|
659
|
+
setThinking(false);
|
|
660
|
+
setMessages((prev) => [
|
|
661
|
+
...prev.filter((m) => m.kind !== "thinking"),
|
|
662
|
+
...(parsed.content
|
|
663
|
+
? [{ kind: "assistant" as const, content: parsed.content }]
|
|
664
|
+
: []),
|
|
665
|
+
{ kind: "tool" as const, toolName: parsed.toolName, label },
|
|
666
|
+
]);
|
|
667
|
+
setPending({
|
|
668
|
+
toolName: parsed.toolName,
|
|
669
|
+
input: parsed.input,
|
|
670
|
+
rawInput: parsed.rawInput,
|
|
671
|
+
remainder: parsed.remainder,
|
|
672
|
+
history: currentHistory,
|
|
673
|
+
});
|
|
541
674
|
}
|
|
542
|
-
|
|
675
|
+
return;
|
|
543
676
|
}
|
|
544
677
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
return (
|
|
554
|
-
<Box flexDirection="column" marginTop={1}>
|
|
555
|
-
<Text color="gray" dimColor>
|
|
556
|
-
{divider}
|
|
557
|
-
</Text>
|
|
558
|
-
<Box paddingX={1} marginBottom={1} gap={2}>
|
|
559
|
-
<Text color={ACCENT} bold>
|
|
560
|
-
COMMIT CHANGES
|
|
561
|
-
</Text>
|
|
562
|
-
</Box>
|
|
563
|
-
|
|
564
|
-
{state.phase === "scanning" && (
|
|
565
|
-
<Box paddingX={1} gap={1}>
|
|
566
|
-
<Text color={ACCENT}>*</Text>
|
|
567
|
-
<Text color="gray" dimColor>
|
|
568
|
-
scanning for changes…
|
|
569
|
-
</Text>
|
|
570
|
-
</Box>
|
|
571
|
-
)}
|
|
572
|
-
|
|
573
|
-
{state.phase === "no-changes" && (
|
|
574
|
-
<Box paddingX={1} flexDirection="column" gap={1}>
|
|
575
|
-
<Box gap={1}>
|
|
576
|
-
<Text color="yellow">!</Text>
|
|
577
|
-
<Text color="white">no uncommitted changes found</Text>
|
|
578
|
-
</Box>
|
|
579
|
-
<Text color="gray" dimColor>
|
|
580
|
-
{" "}
|
|
581
|
-
esc to close
|
|
582
|
-
</Text>
|
|
583
|
-
</Box>
|
|
584
|
-
)}
|
|
585
|
-
|
|
586
|
-
{state.phase === "generating" && (
|
|
587
|
-
<Box paddingX={1} gap={1}>
|
|
588
|
-
<Text color={ACCENT}>*</Text>
|
|
589
|
-
<Text color="gray" dimColor>
|
|
590
|
-
generating commit message…
|
|
591
|
-
</Text>
|
|
592
|
-
</Box>
|
|
593
|
-
)}
|
|
678
|
+
// anything else (changes, clone) — show as text in this context
|
|
679
|
+
setMessages((prev) => [
|
|
680
|
+
...prev.filter((m) => m.kind !== "thinking"),
|
|
681
|
+
{ kind: "assistant", content: raw.trim() },
|
|
682
|
+
]);
|
|
683
|
+
setThinking(false);
|
|
684
|
+
};
|
|
594
685
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
<Text color="gray" dimColor>
|
|
603
|
-
{trunc(state.diff.split("\n")[0] ?? "", w - 20)}
|
|
604
|
-
</Text>
|
|
605
|
-
</Box>
|
|
606
|
-
<Box gap={1} marginTop={1}>
|
|
607
|
-
<Text color="gray" dimColor>
|
|
608
|
-
message:
|
|
609
|
-
</Text>
|
|
610
|
-
</Box>
|
|
686
|
+
const executeAndContinue = async (
|
|
687
|
+
p: PendingTool,
|
|
688
|
+
approved: boolean,
|
|
689
|
+
signal: AbortSignal,
|
|
690
|
+
) => {
|
|
691
|
+
const tool = registry.get(p.toolName);
|
|
692
|
+
if (!tool) return;
|
|
611
693
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
<Text color="white" bold wrap="wrap">
|
|
615
|
-
{state.message}
|
|
616
|
-
</Text>
|
|
617
|
-
<Box gap={3} marginTop={1}>
|
|
618
|
-
<Text color="green">y/enter commit</Text>
|
|
619
|
-
<Text color="cyan">e edit</Text>
|
|
620
|
-
<Text color="gray" dimColor>
|
|
621
|
-
n/esc cancel
|
|
622
|
-
</Text>
|
|
623
|
-
</Box>
|
|
624
|
-
</Box>
|
|
625
|
-
)}
|
|
694
|
+
let result = "(denied by user)";
|
|
695
|
+
let resultKind: string = "text";
|
|
626
696
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
enter to confirm · esc to cancel edit
|
|
640
|
-
</Text>
|
|
641
|
-
</Box>
|
|
642
|
-
)}
|
|
643
|
-
</Box>
|
|
644
|
-
)}
|
|
645
|
-
|
|
646
|
-
{state.phase === "committing" && (
|
|
647
|
-
<Box paddingX={1} gap={1}>
|
|
648
|
-
<Text color={ACCENT}>*</Text>
|
|
649
|
-
<Text color="gray" dimColor>
|
|
650
|
-
committing…
|
|
651
|
-
</Text>
|
|
652
|
-
</Box>
|
|
653
|
-
)}
|
|
697
|
+
if (approved) {
|
|
698
|
+
try {
|
|
699
|
+
const toolResult = await tool.execute(p.input, {
|
|
700
|
+
repoPath,
|
|
701
|
+
messages: p.history,
|
|
702
|
+
});
|
|
703
|
+
result = toolResult.value;
|
|
704
|
+
resultKind = (toolResult as any).kind ?? "text";
|
|
705
|
+
} catch (e: any) {
|
|
706
|
+
result = `Error: ${e.message}`;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
654
709
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
710
|
+
// Image result — write ANSI directly to stdout (bypasses Ink's renderer)
|
|
711
|
+
// and inject an image message into the list instead of a text result.
|
|
712
|
+
if (resultKind === "image" && approved) {
|
|
713
|
+
setMessages((prev) => {
|
|
714
|
+
const next = prev
|
|
715
|
+
.map((m) =>
|
|
716
|
+
m.kind === "tool" &&
|
|
717
|
+
m.toolName === p.toolName &&
|
|
718
|
+
m.result === undefined
|
|
719
|
+
? { ...m, result: "(image)", approved }
|
|
720
|
+
: m,
|
|
721
|
+
)
|
|
722
|
+
.filter((m) => m.kind !== "thinking");
|
|
723
|
+
return [...next, { kind: "image" as const, ansi: result }];
|
|
724
|
+
});
|
|
725
|
+
stdout.write(result + "\n");
|
|
726
|
+
} else {
|
|
727
|
+
// Stamp result onto the tool bubble and remove the trailing thinking bubble
|
|
728
|
+
// in one atomic update — no intermediate render with a dangling spinner.
|
|
729
|
+
setMessages((prev) => {
|
|
730
|
+
const next = prev
|
|
731
|
+
.map((m) =>
|
|
732
|
+
m.kind === "tool" &&
|
|
733
|
+
m.toolName === p.toolName &&
|
|
734
|
+
m.result === undefined
|
|
735
|
+
? { ...m, result, approved }
|
|
736
|
+
: m,
|
|
737
|
+
)
|
|
738
|
+
.filter((m) => m.kind !== "thinking");
|
|
739
|
+
return next;
|
|
740
|
+
});
|
|
741
|
+
}
|
|
663
742
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
<Text color="gray" dimColor>
|
|
673
|
-
{" "}
|
|
674
|
-
enter/esc to close
|
|
675
|
-
</Text>
|
|
676
|
-
</Box>
|
|
677
|
-
)}
|
|
678
|
-
</Box>
|
|
679
|
-
);
|
|
680
|
-
}
|
|
743
|
+
// reload commit list if a write succeeded
|
|
744
|
+
if (
|
|
745
|
+
approved &&
|
|
746
|
+
!result.startsWith("Error") &&
|
|
747
|
+
!result.startsWith("(denied")
|
|
748
|
+
) {
|
|
749
|
+
onReload();
|
|
750
|
+
}
|
|
681
751
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
752
|
+
const nextHistory: Message[] = [
|
|
753
|
+
...p.history,
|
|
754
|
+
{
|
|
755
|
+
role: "user" as const,
|
|
756
|
+
content: approved
|
|
757
|
+
? `Tool result for <${p.toolName}>:\n${result}`
|
|
758
|
+
: `Tool <${p.toolName}> was denied by the user.`,
|
|
759
|
+
type: "text" as const,
|
|
760
|
+
},
|
|
761
|
+
];
|
|
762
|
+
setApiHistory(nextHistory);
|
|
686
763
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
764
|
+
// if the model already wrote a remainder, process it inline
|
|
765
|
+
if (approved && p.remainder) {
|
|
766
|
+
processResponse(p.remainder, nextHistory, signal);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
691
769
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
onCommit: () => void;
|
|
700
|
-
}) {
|
|
701
|
-
const [messages, setMessages] = useState<ChatMsg[]>([]);
|
|
702
|
-
const [input, setInput] = useState("");
|
|
703
|
-
const [thinking, setThinking] = useState(false);
|
|
704
|
-
const [history, setHistory] = useState<
|
|
705
|
-
{ role: "user" | "assistant"; content: string; type: "text" }[]
|
|
706
|
-
>([]);
|
|
707
|
-
|
|
708
|
-
// keywords that mean "commit my changes" in any language
|
|
709
|
-
const COMMIT_TRIGGERS = [
|
|
710
|
-
/commit/i,
|
|
711
|
-
/stage/i,
|
|
712
|
-
/push changes/i,
|
|
713
|
-
// hinglish / hindi
|
|
714
|
-
/commit kr/i,
|
|
715
|
-
/commit kar/i,
|
|
716
|
-
/changes commit/i,
|
|
717
|
-
/changes save/i,
|
|
718
|
-
/save changes/i,
|
|
719
|
-
/badlav.*commit/i,
|
|
720
|
-
];
|
|
721
|
-
|
|
722
|
-
const systemPrompt = `You are a git history analyst embedded in a terminal git timeline viewer.
|
|
723
|
-
You can ONLY answer questions about the git history shown below.
|
|
724
|
-
You CANNOT run commands, execute git operations, or modify files.
|
|
725
|
-
If the user asks to commit, stage, push, or make any git change — reply with exactly: DELEGATE_COMMIT
|
|
726
|
-
Plain text answers only. No markdown. No code blocks. No backticks. Be concise.
|
|
770
|
+
// no remainder — follow-up API call.
|
|
771
|
+
// Set thinking BEFORE the stamp so isBusy never drops to false between
|
|
772
|
+
// the tool completing and the next runChat starting.
|
|
773
|
+
setThinking(true);
|
|
774
|
+
setMessages((prev) => [...prev, { kind: "thinking" }]);
|
|
775
|
+
runChat(nextHistory, signal);
|
|
776
|
+
};
|
|
727
777
|
|
|
728
|
-
|
|
778
|
+
const runChat = async (history: Message[], signal: AbortSignal) => {
|
|
779
|
+
try {
|
|
780
|
+
const raw = await callChat(provider, systemPrompt, history, signal);
|
|
781
|
+
if (signal.aborted) return;
|
|
782
|
+
processResponse(raw, history, signal);
|
|
783
|
+
} catch (e: any) {
|
|
784
|
+
if (e?.name === "AbortError") return;
|
|
785
|
+
setMessages((prev) => [
|
|
786
|
+
...prev.filter((m) => m.kind !== "thinking"),
|
|
787
|
+
{ kind: "assistant", content: `Error: ${String(e)}` },
|
|
788
|
+
]);
|
|
789
|
+
setThinking(false);
|
|
790
|
+
}
|
|
791
|
+
};
|
|
729
792
|
|
|
730
793
|
const ask = async (q: string) => {
|
|
731
|
-
if (!q.trim() || thinking) return;
|
|
794
|
+
if (!q.trim() || thinking || pending !== null) return;
|
|
732
795
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
setMessages((prev) => [...prev, { role: "user", content: q }]);
|
|
736
|
-
setInput("");
|
|
737
|
-
onCommit();
|
|
738
|
-
return;
|
|
739
|
-
}
|
|
796
|
+
const userMsg: Message = { role: "user", content: q, type: "text" };
|
|
797
|
+
const nextHistory = [...apiHistory, userMsg];
|
|
740
798
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
{ role: "user" as const, content: q, type: "text" as const },
|
|
744
|
-
];
|
|
799
|
+
// Set thinking true FIRST so isBusy blocks input before the next render
|
|
800
|
+
setThinking(true);
|
|
745
801
|
setMessages((prev) => [
|
|
746
802
|
...prev,
|
|
747
|
-
{
|
|
748
|
-
{
|
|
803
|
+
{ kind: "user", content: q },
|
|
804
|
+
{ kind: "thinking" },
|
|
749
805
|
]);
|
|
750
|
-
|
|
806
|
+
setApiHistory(nextHistory);
|
|
751
807
|
setInput("");
|
|
752
|
-
try {
|
|
753
|
-
const raw = await callChat(provider, systemPrompt, nextHistory as any);
|
|
754
|
-
const answer = typeof raw === "string" ? raw.trim() : "(no response)";
|
|
755
|
-
|
|
756
|
-
// model-side delegation signal
|
|
757
|
-
if (
|
|
758
|
-
answer === "DELEGATE_COMMIT" ||
|
|
759
|
-
answer.startsWith("DELEGATE_COMMIT")
|
|
760
|
-
) {
|
|
761
|
-
setMessages((prev) => prev.filter((m) => m.role !== "thinking"));
|
|
762
|
-
setThinking(false);
|
|
763
|
-
onCommit();
|
|
764
|
-
return;
|
|
765
|
-
}
|
|
766
808
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
772
|
-
.trim();
|
|
809
|
+
const abort = new AbortController();
|
|
810
|
+
abortRef.current = abort;
|
|
811
|
+
await runChat(nextHistory, abort.signal);
|
|
812
|
+
};
|
|
773
813
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
} finally {
|
|
788
|
-
setThinking(false);
|
|
814
|
+
// permission y/n — only fires when pending !== null
|
|
815
|
+
useInput((inp, key) => {
|
|
816
|
+
if (!pending) return;
|
|
817
|
+
if (inp === "y" || inp === "Y" || key.return) {
|
|
818
|
+
const p = pending;
|
|
819
|
+
setPending(null);
|
|
820
|
+
const abort = abortRef.current ?? new AbortController();
|
|
821
|
+
executeAndContinue(p, true, abort.signal);
|
|
822
|
+
} else if (inp === "n" || inp === "N" || key.escape) {
|
|
823
|
+
const p = pending;
|
|
824
|
+
setPending(null);
|
|
825
|
+
const abort = abortRef.current ?? new AbortController();
|
|
826
|
+
executeAndContinue(p, false, abort.signal);
|
|
789
827
|
}
|
|
790
|
-
};
|
|
828
|
+
});
|
|
791
829
|
|
|
792
830
|
const w = W();
|
|
831
|
+
const isBusy = thinking || pending !== null;
|
|
832
|
+
const hasThinking = messages.some((m) => m.kind === "thinking");
|
|
793
833
|
|
|
794
834
|
return (
|
|
795
835
|
<Box flexDirection="column" marginTop={1}>
|
|
@@ -797,50 +837,120 @@ ${summarizeTimeline(commits)}`;
|
|
|
797
837
|
{"─".repeat(w)}
|
|
798
838
|
</Text>
|
|
799
839
|
|
|
800
|
-
{
|
|
840
|
+
<Box paddingX={1} marginBottom={1} gap={2}>
|
|
841
|
+
<Text color={ACCENT} bold>
|
|
842
|
+
ASK
|
|
843
|
+
</Text>
|
|
844
|
+
<Text color="gray" dimColor>
|
|
845
|
+
git tools available · y/n for writes · esc back
|
|
846
|
+
</Text>
|
|
847
|
+
</Box>
|
|
848
|
+
|
|
801
849
|
{messages.map((msg, i) => {
|
|
802
|
-
|
|
850
|
+
// ── thinking ────────────────────────────────────────────────────
|
|
851
|
+
if (msg.kind === "thinking") {
|
|
852
|
+
// Only render the last thinking bubble; use phrase state directly
|
|
853
|
+
const lastIdx = messages.map((m) => m.kind).lastIndexOf("thinking");
|
|
854
|
+
if (i !== lastIdx) return null;
|
|
803
855
|
return (
|
|
804
|
-
<Box key=
|
|
805
|
-
<Text color={ACCENT}
|
|
806
|
-
<
|
|
807
|
-
thinking…
|
|
808
|
-
</Text>
|
|
856
|
+
<Box key="thinking" gap={1} marginBottom={1}>
|
|
857
|
+
<Text color={ACCENT}>●</Text>
|
|
858
|
+
<TypewriterText key={phrase} text={phrase} />
|
|
809
859
|
</Box>
|
|
810
860
|
);
|
|
811
|
-
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// ── user ────────────────────────────────────────────────────────
|
|
864
|
+
if (msg.kind === "user") {
|
|
812
865
|
return (
|
|
813
|
-
<Box
|
|
866
|
+
<Box
|
|
867
|
+
key={i}
|
|
868
|
+
marginBottom={1}
|
|
869
|
+
gap={1}
|
|
870
|
+
backgroundColor="#1a1a1a"
|
|
871
|
+
paddingLeft={1}
|
|
872
|
+
paddingRight={2}
|
|
873
|
+
>
|
|
814
874
|
<Text color="gray">{">"}</Text>
|
|
815
|
-
<Text color="white">
|
|
875
|
+
<Text color="white" bold>
|
|
876
|
+
{msg.content}
|
|
877
|
+
</Text>
|
|
878
|
+
</Box>
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// ── tool ────────────────────────────────────────────────────────
|
|
883
|
+
if (msg.kind === "tool") {
|
|
884
|
+
const isDone = msg.result !== undefined;
|
|
885
|
+
const denied = msg.approved === false;
|
|
886
|
+
const isError = msg.result?.startsWith("Error") || denied;
|
|
887
|
+
const tool = registry.get(msg.toolName);
|
|
888
|
+
const isWrite = tool && !tool.safe;
|
|
889
|
+
return (
|
|
890
|
+
<Box key={i} flexDirection="column" marginBottom={1}>
|
|
891
|
+
<Box gap={1}>
|
|
892
|
+
<Text color={denied ? "red" : ACCENT}>$</Text>
|
|
893
|
+
<Text color={denied ? "red" : "gray"} dimColor={!denied}>
|
|
894
|
+
{trunc(msg.label, w - 4)}
|
|
895
|
+
</Text>
|
|
896
|
+
{denied && <Text color="red">denied</Text>}
|
|
897
|
+
</Box>
|
|
898
|
+
{!isDone && isWrite && (
|
|
899
|
+
<Box marginLeft={2} gap={1}>
|
|
900
|
+
<Text color="gray">y/enter allow · n/esc deny</Text>
|
|
901
|
+
</Box>
|
|
902
|
+
)}
|
|
903
|
+
{isDone && msg.result && (
|
|
904
|
+
<Box marginLeft={2}>
|
|
905
|
+
<Text color={isError ? "red" : "gray"} dimColor={!isError}>
|
|
906
|
+
{trunc(msg.result.split("\n")[0]!, w - 6)}
|
|
907
|
+
{(msg.result.split("\n")[0]?.length ?? 0) > w - 6
|
|
908
|
+
? "…"
|
|
909
|
+
: ""}
|
|
910
|
+
</Text>
|
|
911
|
+
</Box>
|
|
912
|
+
)}
|
|
913
|
+
</Box>
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// ── image ────────────────────────────────────────────────────────
|
|
918
|
+
// Already written to stdout raw — just show a placeholder label so
|
|
919
|
+
// the message list stays coherent and the image appears above it.
|
|
920
|
+
if (msg.kind === "image") {
|
|
921
|
+
return (
|
|
922
|
+
<Box key={i} gap={1} marginBottom={1}>
|
|
923
|
+
<Text color={ACCENT}>◎</Text>
|
|
924
|
+
<Text color="gray" dimColor>
|
|
925
|
+
image rendered above
|
|
926
|
+
</Text>
|
|
816
927
|
</Box>
|
|
817
928
|
);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// ── assistant ───────────────────────────────────────────────────
|
|
818
932
|
return (
|
|
819
|
-
<Box key={i}
|
|
820
|
-
<Text color={ACCENT}
|
|
821
|
-
<
|
|
822
|
-
{msg.content}
|
|
823
|
-
</Text>
|
|
933
|
+
<Box key={i} marginBottom={1} gap={1}>
|
|
934
|
+
<Text color={ACCENT}>●</Text>
|
|
935
|
+
<MsgBody content={msg.content} />
|
|
824
936
|
</Box>
|
|
825
937
|
);
|
|
826
938
|
})}
|
|
827
939
|
|
|
828
|
-
{
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
)}
|
|
843
|
-
</Box>
|
|
940
|
+
{pending && (
|
|
941
|
+
<Box marginLeft={2} gap={1} marginBottom={1}>
|
|
942
|
+
<Text color="gray">y/enter allow · n/esc deny</Text>
|
|
943
|
+
</Box>
|
|
944
|
+
)}
|
|
945
|
+
|
|
946
|
+
<InputBox
|
|
947
|
+
value={input}
|
|
948
|
+
onChange={setInput}
|
|
949
|
+
onSubmit={(v) => {
|
|
950
|
+
if (v.trim()) ask(v.trim());
|
|
951
|
+
}}
|
|
952
|
+
inputKey={isBusy ? 1 : 0}
|
|
953
|
+
/>
|
|
844
954
|
</Box>
|
|
845
955
|
);
|
|
846
956
|
}
|
|
@@ -851,8 +961,7 @@ type UIMode =
|
|
|
851
961
|
| { type: "browse" }
|
|
852
962
|
| { type: "search"; query: string }
|
|
853
963
|
| { type: "ask" }
|
|
854
|
-
| { type: "revert"; commit: Commit }
|
|
855
|
-
| { type: "commit" };
|
|
964
|
+
| { type: "revert"; commit: Commit };
|
|
856
965
|
|
|
857
966
|
type StatusMsg = { id: number; text: string; ok: boolean };
|
|
858
967
|
let sid = 0;
|
|
@@ -964,12 +1073,7 @@ export function TimelineRunner({
|
|
|
964
1073
|
else process.exit(0);
|
|
965
1074
|
}
|
|
966
1075
|
|
|
967
|
-
|
|
968
|
-
if (
|
|
969
|
-
mode.type === "ask" ||
|
|
970
|
-
mode.type === "revert" ||
|
|
971
|
-
mode.type === "commit"
|
|
972
|
-
) {
|
|
1076
|
+
if (mode.type === "ask" || mode.type === "revert") {
|
|
973
1077
|
if (key.escape) setMode({ type: "browse" });
|
|
974
1078
|
return;
|
|
975
1079
|
}
|
|
@@ -979,7 +1083,6 @@ export function TimelineRunner({
|
|
|
979
1083
|
return;
|
|
980
1084
|
}
|
|
981
1085
|
|
|
982
|
-
// diff open
|
|
983
1086
|
if (showDiff) {
|
|
984
1087
|
if (key.escape || input === "d") {
|
|
985
1088
|
setShowDiff(false);
|
|
@@ -1012,25 +1115,18 @@ export function TimelineRunner({
|
|
|
1012
1115
|
setMode({ type: "search", query: "" });
|
|
1013
1116
|
return;
|
|
1014
1117
|
}
|
|
1015
|
-
if (input === "?") {
|
|
1118
|
+
if (input === "?" || input === "a" || input === "A") {
|
|
1016
1119
|
setMode({ type: "ask" });
|
|
1017
1120
|
return;
|
|
1018
1121
|
}
|
|
1019
|
-
if (input === "c" || input === "C") {
|
|
1020
|
-
setMode({ type: "commit" });
|
|
1021
|
-
return;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
1122
|
if (key.return && selected) {
|
|
1025
1123
|
setShowDiff(true);
|
|
1026
1124
|
return;
|
|
1027
1125
|
}
|
|
1028
|
-
|
|
1029
1126
|
if (input === "x" || input === "X") {
|
|
1030
1127
|
if (selected) setMode({ type: "revert", commit: selected });
|
|
1031
1128
|
return;
|
|
1032
1129
|
}
|
|
1033
|
-
|
|
1034
1130
|
if (key.upArrow) {
|
|
1035
1131
|
const next = Math.max(0, selectedIdx - 1);
|
|
1036
1132
|
setSelectedIdx(next);
|
|
@@ -1038,7 +1134,6 @@ export function TimelineRunner({
|
|
|
1038
1134
|
if (next < scrollOffset) setScrollOffset(next);
|
|
1039
1135
|
return;
|
|
1040
1136
|
}
|
|
1041
|
-
|
|
1042
1137
|
if (key.downArrow) {
|
|
1043
1138
|
const next = Math.min(filtered.length - 1, selectedIdx + 1);
|
|
1044
1139
|
setSelectedIdx(next);
|
|
@@ -1069,7 +1164,6 @@ export function TimelineRunner({
|
|
|
1069
1164
|
const isSearching = mode.type === "search";
|
|
1070
1165
|
const isAsking = mode.type === "ask";
|
|
1071
1166
|
const isReverting = mode.type === "revert";
|
|
1072
|
-
const isCommitting = mode.type === "commit";
|
|
1073
1167
|
const searchQuery = isSearching ? mode.query : "";
|
|
1074
1168
|
const visible = filtered.slice(scrollOffset, scrollOffset + visibleCount);
|
|
1075
1169
|
|
|
@@ -1078,10 +1172,10 @@ export function TimelineRunner({
|
|
|
1078
1172
|
: isSearching
|
|
1079
1173
|
? "type to filter · enter confirm · esc cancel"
|
|
1080
1174
|
: isAsking
|
|
1081
|
-
? "
|
|
1082
|
-
: isReverting
|
|
1083
|
-
? "
|
|
1084
|
-
: `↑↓ navigate · enter diff · x revert ·
|
|
1175
|
+
? "ask anything · git tools available · esc back"
|
|
1176
|
+
: isReverting
|
|
1177
|
+
? "y confirm · n/esc cancel"
|
|
1178
|
+
: `↑↓ navigate · enter diff · x revert · a ask · / search${onExit ? " · q back" : " · ^C exit"}`;
|
|
1085
1179
|
|
|
1086
1180
|
return (
|
|
1087
1181
|
<Box flexDirection="column">
|
|
@@ -1101,7 +1195,7 @@ export function TimelineRunner({
|
|
|
1101
1195
|
)}
|
|
1102
1196
|
</Box>
|
|
1103
1197
|
|
|
1104
|
-
{/* status messages
|
|
1198
|
+
{/* status messages */}
|
|
1105
1199
|
<Static items={statusMsgs}>
|
|
1106
1200
|
{(msg) => (
|
|
1107
1201
|
<Box key={msg.id} paddingX={1} gap={1}>
|
|
@@ -1167,21 +1261,8 @@ export function TimelineRunner({
|
|
|
1167
1261
|
if (msg) {
|
|
1168
1262
|
addStatus(msg, true);
|
|
1169
1263
|
reloadCommits();
|
|
1170
|
-
} else
|
|
1171
|
-
|
|
1172
|
-
/>
|
|
1173
|
-
)}
|
|
1174
|
-
|
|
1175
|
-
{/* commit overlay */}
|
|
1176
|
-
{isCommitting && provider && (
|
|
1177
|
-
<CommitPanel
|
|
1178
|
-
repoPath={repoPath}
|
|
1179
|
-
provider={provider}
|
|
1180
|
-
onDone={(msg) => {
|
|
1181
|
-
setMode({ type: "browse" });
|
|
1182
|
-
if (msg) {
|
|
1183
|
-
addStatus(`committed: ${trunc(msg, 60)}`, true);
|
|
1184
|
-
reloadCommits();
|
|
1264
|
+
} else {
|
|
1265
|
+
addStatus("revert cancelled", false);
|
|
1185
1266
|
}
|
|
1186
1267
|
}}
|
|
1187
1268
|
/>
|
|
@@ -1191,9 +1272,11 @@ export function TimelineRunner({
|
|
|
1191
1272
|
{isAsking && provider && (
|
|
1192
1273
|
<AskPanel
|
|
1193
1274
|
commits={commits}
|
|
1275
|
+
repoPath={repoPath}
|
|
1194
1276
|
provider={provider}
|
|
1195
|
-
|
|
1196
|
-
|
|
1277
|
+
onReload={() => {
|
|
1278
|
+
reloadCommits();
|
|
1279
|
+
addStatus("commits reloaded", true);
|
|
1197
1280
|
}}
|
|
1198
1281
|
/>
|
|
1199
1282
|
)}
|