pi-crew 0.1.30 → 0.1.32
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/CHANGELOG.md +9 -0
- package/README.md +80 -27
- package/agents/analyst.md +11 -11
- package/agents/critic.md +11 -11
- package/agents/executor.md +11 -11
- package/agents/explorer.md +11 -11
- package/agents/planner.md +11 -11
- package/agents/reviewer.md +11 -11
- package/agents/security-reviewer.md +11 -11
- package/agents/test-engineer.md +11 -11
- package/agents/verifier.md +11 -11
- package/agents/writer.md +11 -11
- package/docs/architecture.md +173 -164
- package/docs/refactor-tasks-phase3.md +394 -394
- package/docs/refactor-tasks-phase4.md +564 -564
- package/docs/refactor-tasks-phase5.md +402 -402
- package/docs/refactor-tasks-phase6.md +662 -662
- package/docs/research-extension-examples.md +297 -0
- package/docs/research-extension-system.md +324 -0
- package/docs/research-optimization-plan.md +548 -0
- package/docs/research-pi-coding-agent.md +357 -0
- package/docs/resource-formats.md +4 -3
- package/docs/runtime-flow.md +148 -148
- package/docs/source-runtime-refactor-map.md +83 -83
- package/docs/usage.md +3 -3
- package/index.ts +6 -6
- package/package.json +1 -1
- package/schema.json +53 -1
- package/skills/git-master/SKILL.md +24 -19
- package/skills/read-only-explorer/SKILL.md +26 -21
- package/skills/safe-bash/SKILL.md +21 -16
- package/skills/task-packet/SKILL.md +28 -23
- package/skills/verify-evidence/SKILL.md +27 -22
- package/src/agents/agent-serializer.ts +34 -34
- package/src/agents/discover-agents.ts +102 -102
- package/src/config/config.ts +14 -1
- package/src/config/defaults.ts +3 -2
- package/src/extension/cross-extension-rpc.ts +82 -82
- package/src/extension/import-index.ts +4 -3
- package/src/extension/management.ts +2 -2
- package/src/extension/project-init.ts +9 -7
- package/src/extension/register.ts +63 -0
- package/src/extension/registration/artifact-cleanup.ts +15 -14
- package/src/extension/registration/commands.ts +208 -208
- package/src/extension/registration/compaction-guard.ts +125 -0
- package/src/extension/registration/subagent-tools.ts +27 -8
- package/src/extension/registration/team-tool.ts +61 -44
- package/src/extension/result-watcher.ts +98 -98
- package/src/extension/run-import.ts +4 -4
- package/src/extension/run-index.ts +14 -14
- package/src/extension/run-maintenance.ts +24 -24
- package/src/extension/team-tool/api.ts +3 -0
- package/src/extension/team-tool/cancel.ts +31 -31
- package/src/extension/team-tool/doctor.ts +179 -178
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +79 -79
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/status.ts +73 -73
- package/src/prompt/prompt-runtime.ts +68 -68
- package/src/runtime/agent-control.ts +64 -64
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +113 -113
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/background-runner.ts +53 -53
- package/src/runtime/crew-agent-runtime.ts +58 -58
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +88 -88
- package/src/runtime/live-agent-control.ts +78 -78
- package/src/runtime/live-agent-manager.ts +85 -85
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +299 -299
- package/src/runtime/manifest-cache.ts +212 -214
- package/src/runtime/model-fallback.ts +261 -261
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +99 -99
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/pi-spawn.ts +96 -96
- package/src/runtime/policy-engine.ts +78 -78
- package/src/runtime/post-exit-stdio-guard.ts +86 -86
- package/src/runtime/process-status.ts +56 -56
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +28 -28
- package/src/runtime/subagent-manager.ts +29 -2
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-output-context.ts +106 -106
- package/src/runtime/task-packet.ts +84 -84
- package/src/runtime/task-runner/live-executor.ts +98 -98
- package/src/runtime/task-runner/progress.ts +111 -111
- package/src/runtime/task-runner/prompt-builder.ts +72 -72
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/schema/config-schema.ts +12 -0
- package/src/schema/team-tool-schema.ts +100 -100
- package/src/state/artifact-store.ts +108 -108
- package/src/state/contracts.ts +105 -105
- package/src/state/jsonl-writer.ts +77 -77
- package/src/state/state-store.ts +8 -9
- package/src/state/task-claims.ts +42 -42
- package/src/state/types.ts +180 -180
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/discover-teams.ts +2 -2
- package/src/teams/team-serializer.ts +36 -36
- package/src/types/diff.d.ts +18 -0
- package/src/ui/crew-footer.ts +101 -101
- package/src/ui/crew-select-list.ts +111 -111
- package/src/ui/crew-widget.ts +285 -285
- package/src/ui/dynamic-border.ts +25 -25
- package/src/ui/layout-primitives.ts +106 -106
- package/src/ui/loaders.ts +158 -158
- package/src/ui/mascot.ts +441 -441
- package/src/ui/powerbar-publisher.ts +94 -94
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/run-dashboard.ts +372 -372
- package/src/ui/status-colors.ts +54 -54
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/ui/transcript-viewer.ts +302 -302
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/file-coalescer.ts +84 -84
- package/src/utils/frontmatter.ts +36 -36
- package/src/utils/fs-watch.ts +31 -31
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +12 -12
- package/src/utils/names.ts +26 -26
- package/src/utils/paths.ts +34 -7
- package/src/utils/sleep.ts +32 -32
- package/src/utils/timings.ts +31 -31
- package/src/utils/visual.ts +159 -159
- package/src/workflows/discover-workflows.ts +2 -2
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/worktree/branch-freshness.ts +45 -45
- package/src/worktree/cleanup.ts +71 -69
- package/src/worktree/worktree-manager.ts +3 -1
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/workflows/default.workflow.md +29 -29
- package/workflows/fast-fix.workflow.md +22 -22
- package/workflows/implementation.workflow.md +38 -38
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
|
@@ -1,94 +1,94 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import { listRecentRuns } from "../extension/run-index.ts";
|
|
3
|
-
import type { CrewUiConfig } from "../config/config.ts";
|
|
4
|
-
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
5
|
-
import { readJsonFileCoalesced } from "../utils/file-coalescer.ts";
|
|
6
|
-
import type { TeamTaskState, TeamRunManifest } from "../state/types.ts";
|
|
7
|
-
import { aggregateUsage } from "../state/usage.ts";
|
|
8
|
-
import { isDisplayActiveRun } from "../runtime/process-status.ts";
|
|
9
|
-
import { logInternalError } from "../utils/internal-error.ts";
|
|
10
|
-
import type { ManifestCache } from "../runtime/manifest-cache.ts";
|
|
11
|
-
|
|
12
|
-
type EventBus = { emit?: (event: string, data: unknown) => void } | undefined;
|
|
13
|
-
|
|
14
|
-
const TASK_READ_TTL_MS = 200;
|
|
15
|
-
|
|
16
|
-
function safeEmit(events: EventBus, event: string, data: unknown): void {
|
|
17
|
-
try {
|
|
18
|
-
events?.emit?.(event, data);
|
|
19
|
-
} catch (error) {
|
|
20
|
-
logInternalError("powerbar.safeEmit", error, `event=${event}`);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function readTasks(tasksPath: string): TeamTaskState[] {
|
|
25
|
-
try {
|
|
26
|
-
const parse = () => {
|
|
27
|
-
const parsed = JSON.parse(fs.readFileSync(tasksPath, "utf-8"));
|
|
28
|
-
return Array.isArray(parsed) ? (parsed as TeamTaskState[]) : [];
|
|
29
|
-
};
|
|
30
|
-
return readJsonFileCoalesced(tasksPath, TASK_READ_TTL_MS, parse);
|
|
31
|
-
} catch (error) {
|
|
32
|
-
logInternalError("powerbar.readTasks", error, tasksPath);
|
|
33
|
-
return [];
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function compactTokens(total: number): string {
|
|
38
|
-
return total >= 1000 ? `${Math.round(total / 1000)}k` : `${total}`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function registerPiCrewPowerbarSegments(events: EventBus, config?: CrewUiConfig): void {
|
|
42
|
-
if (config?.powerbar === false) return;
|
|
43
|
-
safeEmit(events, "powerbar:register-segment", { id: "pi-crew-active", label: "pi-crew active agents" });
|
|
44
|
-
safeEmit(events, "powerbar:register-segment", { id: "pi-crew-progress", label: "pi-crew run progress" });
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function updatePiCrewPowerbar(events: EventBus, cwd: string, config?: CrewUiConfig, manifestCache?: ManifestCache): void {
|
|
48
|
-
if (config?.powerbar === false) return;
|
|
49
|
-
const runs = manifestCache ? manifestCache.list(20) : listRecentRuns(cwd, 20);
|
|
50
|
-
const active = runs.map((run) => {
|
|
51
|
-
let agents: ReturnType<typeof readCrewAgents> = [];
|
|
52
|
-
try {
|
|
53
|
-
agents = readCrewAgents(run);
|
|
54
|
-
} catch (error) {
|
|
55
|
-
logInternalError("powerbar.readCrewAgents", error, run.runId);
|
|
56
|
-
}
|
|
57
|
-
return { run, agents };
|
|
58
|
-
}).filter((item) => isDisplayActiveRun(item.run, item.agents));
|
|
59
|
-
if (!active.length) {
|
|
60
|
-
safeEmit(events, "powerbar:update", { id: "pi-crew-active" });
|
|
61
|
-
safeEmit(events, "powerbar:update", { id: "pi-crew-progress" });
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
const agents = active.flatMap((item) => item.agents);
|
|
65
|
-
const tasks = active.flatMap((item) => readTasks(item.run.tasksPath));
|
|
66
|
-
const running = agents.filter((agent) => agent.status === "running").length;
|
|
67
|
-
const waiting = tasks.filter((task) => task.status === "queued").length;
|
|
68
|
-
const completed = tasks.filter((task) => task.status === "completed").length;
|
|
69
|
-
const total = Math.max(1, tasks.length || agents.length);
|
|
70
|
-
const usage = aggregateUsage(tasks);
|
|
71
|
-
const tokenTotal = usage ? (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0) : 0;
|
|
72
|
-
const model = config?.showModel === false ? undefined : agents.find((agent) => agent.model)?.model?.split("/").at(-1);
|
|
73
|
-
const tokenText = config?.showTokens === false || !tokenTotal ? undefined : compactTokens(tokenTotal);
|
|
74
|
-
safeEmit(events, "powerbar:update", {
|
|
75
|
-
id: "pi-crew-active",
|
|
76
|
-
icon: "⚙",
|
|
77
|
-
text: `crew ${running}a/${waiting}w`,
|
|
78
|
-
suffix: [model, tokenText].filter(Boolean).join(" · ") || undefined,
|
|
79
|
-
color: running ? "accent" : "warning",
|
|
80
|
-
});
|
|
81
|
-
safeEmit(events, "powerbar:update", {
|
|
82
|
-
id: "pi-crew-progress",
|
|
83
|
-
text: (active[0]?.run as TeamRunManifest)?.team ?? "crew",
|
|
84
|
-
bar: Math.round((completed / total) * 100),
|
|
85
|
-
suffix: `${completed}/${total}${tokenText ? ` · ${tokenText}` : ""}`,
|
|
86
|
-
color: completed === total ? "success" : "accent",
|
|
87
|
-
barSegments: 8,
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function clearPiCrewPowerbar(events: EventBus): void {
|
|
92
|
-
safeEmit(events, "powerbar:update", { id: "pi-crew-active" });
|
|
93
|
-
safeEmit(events, "powerbar:update", { id: "pi-crew-progress" });
|
|
94
|
-
}
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { listRecentRuns } from "../extension/run-index.ts";
|
|
3
|
+
import type { CrewUiConfig } from "../config/config.ts";
|
|
4
|
+
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
5
|
+
import { readJsonFileCoalesced } from "../utils/file-coalescer.ts";
|
|
6
|
+
import type { TeamTaskState, TeamRunManifest } from "../state/types.ts";
|
|
7
|
+
import { aggregateUsage } from "../state/usage.ts";
|
|
8
|
+
import { isDisplayActiveRun } from "../runtime/process-status.ts";
|
|
9
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
10
|
+
import type { ManifestCache } from "../runtime/manifest-cache.ts";
|
|
11
|
+
|
|
12
|
+
type EventBus = { emit?: (event: string, data: unknown) => void } | undefined;
|
|
13
|
+
|
|
14
|
+
const TASK_READ_TTL_MS = 200;
|
|
15
|
+
|
|
16
|
+
function safeEmit(events: EventBus, event: string, data: unknown): void {
|
|
17
|
+
try {
|
|
18
|
+
events?.emit?.(event, data);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
logInternalError("powerbar.safeEmit", error, `event=${event}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readTasks(tasksPath: string): TeamTaskState[] {
|
|
25
|
+
try {
|
|
26
|
+
const parse = () => {
|
|
27
|
+
const parsed = JSON.parse(fs.readFileSync(tasksPath, "utf-8"));
|
|
28
|
+
return Array.isArray(parsed) ? (parsed as TeamTaskState[]) : [];
|
|
29
|
+
};
|
|
30
|
+
return readJsonFileCoalesced(tasksPath, TASK_READ_TTL_MS, parse);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
logInternalError("powerbar.readTasks", error, tasksPath);
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function compactTokens(total: number): string {
|
|
38
|
+
return total >= 1000 ? `${Math.round(total / 1000)}k` : `${total}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function registerPiCrewPowerbarSegments(events: EventBus, config?: CrewUiConfig): void {
|
|
42
|
+
if (config?.powerbar === false) return;
|
|
43
|
+
safeEmit(events, "powerbar:register-segment", { id: "pi-crew-active", label: "pi-crew active agents" });
|
|
44
|
+
safeEmit(events, "powerbar:register-segment", { id: "pi-crew-progress", label: "pi-crew run progress" });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function updatePiCrewPowerbar(events: EventBus, cwd: string, config?: CrewUiConfig, manifestCache?: ManifestCache): void {
|
|
48
|
+
if (config?.powerbar === false) return;
|
|
49
|
+
const runs = manifestCache ? manifestCache.list(20) : listRecentRuns(cwd, 20);
|
|
50
|
+
const active = runs.map((run) => {
|
|
51
|
+
let agents: ReturnType<typeof readCrewAgents> = [];
|
|
52
|
+
try {
|
|
53
|
+
agents = readCrewAgents(run);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
logInternalError("powerbar.readCrewAgents", error, run.runId);
|
|
56
|
+
}
|
|
57
|
+
return { run, agents };
|
|
58
|
+
}).filter((item) => isDisplayActiveRun(item.run, item.agents));
|
|
59
|
+
if (!active.length) {
|
|
60
|
+
safeEmit(events, "powerbar:update", { id: "pi-crew-active" });
|
|
61
|
+
safeEmit(events, "powerbar:update", { id: "pi-crew-progress" });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const agents = active.flatMap((item) => item.agents);
|
|
65
|
+
const tasks = active.flatMap((item) => readTasks(item.run.tasksPath));
|
|
66
|
+
const running = agents.filter((agent) => agent.status === "running").length;
|
|
67
|
+
const waiting = tasks.filter((task) => task.status === "queued").length;
|
|
68
|
+
const completed = tasks.filter((task) => task.status === "completed").length;
|
|
69
|
+
const total = Math.max(1, tasks.length || agents.length);
|
|
70
|
+
const usage = aggregateUsage(tasks);
|
|
71
|
+
const tokenTotal = usage ? (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0) : 0;
|
|
72
|
+
const model = config?.showModel === false ? undefined : agents.find((agent) => agent.model)?.model?.split("/").at(-1);
|
|
73
|
+
const tokenText = config?.showTokens === false || !tokenTotal ? undefined : compactTokens(tokenTotal);
|
|
74
|
+
safeEmit(events, "powerbar:update", {
|
|
75
|
+
id: "pi-crew-active",
|
|
76
|
+
icon: "⚙",
|
|
77
|
+
text: `crew ${running}a/${waiting}w`,
|
|
78
|
+
suffix: [model, tokenText].filter(Boolean).join(" · ") || undefined,
|
|
79
|
+
color: running ? "accent" : "warning",
|
|
80
|
+
});
|
|
81
|
+
safeEmit(events, "powerbar:update", {
|
|
82
|
+
id: "pi-crew-progress",
|
|
83
|
+
text: (active[0]?.run as TeamRunManifest)?.team ?? "crew",
|
|
84
|
+
bar: Math.round((completed / total) * 100),
|
|
85
|
+
suffix: `${completed}/${total}${tokenText ? ` · ${tokenText}` : ""}`,
|
|
86
|
+
color: completed === total ? "success" : "accent",
|
|
87
|
+
barSegments: 8,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function clearPiCrewPowerbar(events: EventBus): void {
|
|
92
|
+
safeEmit(events, "powerbar:update", { id: "pi-crew-active" });
|
|
93
|
+
safeEmit(events, "powerbar:update", { id: "pi-crew-progress" });
|
|
94
|
+
}
|
package/src/ui/render-diff.ts
CHANGED
|
@@ -1,119 +1,119 @@
|
|
|
1
|
-
import * as Diff from "diff";
|
|
2
|
-
import type { CrewTheme } from "./theme-adapter.ts";
|
|
3
|
-
import { asCrewTheme } from "./theme-adapter.ts";
|
|
4
|
-
|
|
5
|
-
interface ParsedDiffLine {
|
|
6
|
-
prefix: string;
|
|
7
|
-
lineNum: string; content: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
interface DiffLineContent {
|
|
11
|
-
lineNum: string;
|
|
12
|
-
content: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function parseDiffLine(line: string): ParsedDiffLine | null {
|
|
16
|
-
const match = line.match(/^([+-\s])(\s*\d*)\s(.*)$/);
|
|
17
|
-
if (!match) return null;
|
|
18
|
-
return { prefix: match[1], lineNum: match[2], content: match[3] };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function replaceTabs(text: string): string {
|
|
22
|
-
return text.replace(/\t/g, " ");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function renderIntraLineDiff(theme: CrewTheme, oldContent: string, newContent: string): { removedLine: string; addedLine: string } {
|
|
26
|
-
const wordDiff = Diff.diffWords(oldContent, newContent);
|
|
27
|
-
let removedLine = "";
|
|
28
|
-
let addedLine = "";
|
|
29
|
-
let isFirstRemoved = true;
|
|
30
|
-
let isFirstAdded = true;
|
|
31
|
-
|
|
32
|
-
for (const part of wordDiff) {
|
|
33
|
-
if (part.removed) {
|
|
34
|
-
let value = part.value;
|
|
35
|
-
if (isFirstRemoved) {
|
|
36
|
-
const leadingWs = value.match(/^(\s*)/)?.[1] ?? "";
|
|
37
|
-
value = value.slice(leadingWs.length);
|
|
38
|
-
removedLine += leadingWs;
|
|
39
|
-
isFirstRemoved = false;
|
|
40
|
-
}
|
|
41
|
-
if (value) removedLine += theme.inverse?.(value) ?? value;
|
|
42
|
-
} else if (part.added) {
|
|
43
|
-
let value = part.value;
|
|
44
|
-
if (isFirstAdded) {
|
|
45
|
-
const leadingWs = value.match(/^(\s*)/)?.[1] ?? "";
|
|
46
|
-
value = value.slice(leadingWs.length);
|
|
47
|
-
addedLine += leadingWs;
|
|
48
|
-
isFirstAdded = false;
|
|
49
|
-
}
|
|
50
|
-
if (value) addedLine += theme.inverse?.(value) ?? value;
|
|
51
|
-
} else {
|
|
52
|
-
removedLine += part.value;
|
|
53
|
-
addedLine += part.value;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return { removedLine, addedLine };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface RenderDiffOptions {
|
|
61
|
-
filePath?: string;
|
|
62
|
-
theme?: unknown;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function renderDiff(diffText: string, options: RenderDiffOptions = {}): string {
|
|
66
|
-
const theme = asCrewTheme(options.theme);
|
|
67
|
-
const lines = diffText.split("\n");
|
|
68
|
-
const result: string[] = [];
|
|
69
|
-
let i = 0;
|
|
70
|
-
|
|
71
|
-
while (i < lines.length) {
|
|
72
|
-
const line = lines[i] ?? "";
|
|
73
|
-
const parsed = parseDiffLine(line);
|
|
74
|
-
if (!parsed) {
|
|
75
|
-
result.push(theme.fg("toolDiffContext", line));
|
|
76
|
-
i++;
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (parsed.prefix === "-") {
|
|
81
|
-
const removedLines: DiffLineContent[] = [];
|
|
82
|
-
while (i < lines.length) {
|
|
83
|
-
const nextParsed = parseDiffLine(lines[i] ?? "");
|
|
84
|
-
if (!nextParsed || nextParsed.prefix !== "-") break;
|
|
85
|
-
removedLines.push({ lineNum: nextParsed.lineNum, content: nextParsed.content });
|
|
86
|
-
i++;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const addedLines: DiffLineContent[] = [];
|
|
90
|
-
while (i < lines.length) {
|
|
91
|
-
const nextParsed = parseDiffLine(lines[i] ?? "");
|
|
92
|
-
if (!nextParsed || nextParsed.prefix !== "+") break;
|
|
93
|
-
addedLines.push({ lineNum: nextParsed.lineNum, content: nextParsed.content });
|
|
94
|
-
i++;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (removedLines.length === 1 && addedLines.length === 1) {
|
|
98
|
-
const { removedLine, addedLine } = renderIntraLineDiff(theme, replaceTabs(removedLines[0]!.content), replaceTabs(addedLines[0]!.content));
|
|
99
|
-
result.push(theme.fg("toolDiffRemoved", `-${removedLines[0]!.lineNum} ${removedLine}`));
|
|
100
|
-
result.push(theme.fg("toolDiffAdded", `+${addedLines[0]!.lineNum} ${addedLine}`));
|
|
101
|
-
} else {
|
|
102
|
-
for (const removed of removedLines) {
|
|
103
|
-
result.push(theme.fg("toolDiffRemoved", `-${removed.lineNum} ${replaceTabs(removed.content)}`));
|
|
104
|
-
}
|
|
105
|
-
for (const added of addedLines) {
|
|
106
|
-
result.push(theme.fg("toolDiffAdded", `+${added.lineNum} ${replaceTabs(added.content)}`));
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
} else if (parsed.prefix === "+") {
|
|
110
|
-
result.push(theme.fg("toolDiffAdded", `+${parsed.lineNum} ${replaceTabs(parsed.content)}`));
|
|
111
|
-
i++;
|
|
112
|
-
} else {
|
|
113
|
-
result.push(theme.fg("toolDiffContext", ` ${parsed.lineNum} ${replaceTabs(parsed.content)}`));
|
|
114
|
-
i++;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return result.join("\n");
|
|
119
|
-
}
|
|
1
|
+
import * as Diff from "diff";
|
|
2
|
+
import type { CrewTheme } from "./theme-adapter.ts";
|
|
3
|
+
import { asCrewTheme } from "./theme-adapter.ts";
|
|
4
|
+
|
|
5
|
+
interface ParsedDiffLine {
|
|
6
|
+
prefix: string;
|
|
7
|
+
lineNum: string; content: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface DiffLineContent {
|
|
11
|
+
lineNum: string;
|
|
12
|
+
content: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseDiffLine(line: string): ParsedDiffLine | null {
|
|
16
|
+
const match = line.match(/^([+-\s])(\s*\d*)\s(.*)$/);
|
|
17
|
+
if (!match) return null;
|
|
18
|
+
return { prefix: match[1], lineNum: match[2], content: match[3] };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function replaceTabs(text: string): string {
|
|
22
|
+
return text.replace(/\t/g, " ");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function renderIntraLineDiff(theme: CrewTheme, oldContent: string, newContent: string): { removedLine: string; addedLine: string } {
|
|
26
|
+
const wordDiff = Diff.diffWords(oldContent, newContent);
|
|
27
|
+
let removedLine = "";
|
|
28
|
+
let addedLine = "";
|
|
29
|
+
let isFirstRemoved = true;
|
|
30
|
+
let isFirstAdded = true;
|
|
31
|
+
|
|
32
|
+
for (const part of wordDiff) {
|
|
33
|
+
if (part.removed) {
|
|
34
|
+
let value = part.value;
|
|
35
|
+
if (isFirstRemoved) {
|
|
36
|
+
const leadingWs = value.match(/^(\s*)/)?.[1] ?? "";
|
|
37
|
+
value = value.slice(leadingWs.length);
|
|
38
|
+
removedLine += leadingWs;
|
|
39
|
+
isFirstRemoved = false;
|
|
40
|
+
}
|
|
41
|
+
if (value) removedLine += theme.inverse?.(value) ?? value;
|
|
42
|
+
} else if (part.added) {
|
|
43
|
+
let value = part.value;
|
|
44
|
+
if (isFirstAdded) {
|
|
45
|
+
const leadingWs = value.match(/^(\s*)/)?.[1] ?? "";
|
|
46
|
+
value = value.slice(leadingWs.length);
|
|
47
|
+
addedLine += leadingWs;
|
|
48
|
+
isFirstAdded = false;
|
|
49
|
+
}
|
|
50
|
+
if (value) addedLine += theme.inverse?.(value) ?? value;
|
|
51
|
+
} else {
|
|
52
|
+
removedLine += part.value;
|
|
53
|
+
addedLine += part.value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { removedLine, addedLine };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface RenderDiffOptions {
|
|
61
|
+
filePath?: string;
|
|
62
|
+
theme?: unknown;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function renderDiff(diffText: string, options: RenderDiffOptions = {}): string {
|
|
66
|
+
const theme = asCrewTheme(options.theme);
|
|
67
|
+
const lines = diffText.split("\n");
|
|
68
|
+
const result: string[] = [];
|
|
69
|
+
let i = 0;
|
|
70
|
+
|
|
71
|
+
while (i < lines.length) {
|
|
72
|
+
const line = lines[i] ?? "";
|
|
73
|
+
const parsed = parseDiffLine(line);
|
|
74
|
+
if (!parsed) {
|
|
75
|
+
result.push(theme.fg("toolDiffContext", line));
|
|
76
|
+
i++;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (parsed.prefix === "-") {
|
|
81
|
+
const removedLines: DiffLineContent[] = [];
|
|
82
|
+
while (i < lines.length) {
|
|
83
|
+
const nextParsed = parseDiffLine(lines[i] ?? "");
|
|
84
|
+
if (!nextParsed || nextParsed.prefix !== "-") break;
|
|
85
|
+
removedLines.push({ lineNum: nextParsed.lineNum, content: nextParsed.content });
|
|
86
|
+
i++;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const addedLines: DiffLineContent[] = [];
|
|
90
|
+
while (i < lines.length) {
|
|
91
|
+
const nextParsed = parseDiffLine(lines[i] ?? "");
|
|
92
|
+
if (!nextParsed || nextParsed.prefix !== "+") break;
|
|
93
|
+
addedLines.push({ lineNum: nextParsed.lineNum, content: nextParsed.content });
|
|
94
|
+
i++;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (removedLines.length === 1 && addedLines.length === 1) {
|
|
98
|
+
const { removedLine, addedLine } = renderIntraLineDiff(theme, replaceTabs(removedLines[0]!.content), replaceTabs(addedLines[0]!.content));
|
|
99
|
+
result.push(theme.fg("toolDiffRemoved", `-${removedLines[0]!.lineNum} ${removedLine}`));
|
|
100
|
+
result.push(theme.fg("toolDiffAdded", `+${addedLines[0]!.lineNum} ${addedLine}`));
|
|
101
|
+
} else {
|
|
102
|
+
for (const removed of removedLines) {
|
|
103
|
+
result.push(theme.fg("toolDiffRemoved", `-${removed.lineNum} ${replaceTabs(removed.content)}`));
|
|
104
|
+
}
|
|
105
|
+
for (const added of addedLines) {
|
|
106
|
+
result.push(theme.fg("toolDiffAdded", `+${added.lineNum} ${replaceTabs(added.content)}`));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} else if (parsed.prefix === "+") {
|
|
110
|
+
result.push(theme.fg("toolDiffAdded", `+${parsed.lineNum} ${replaceTabs(parsed.content)}`));
|
|
111
|
+
i++;
|
|
112
|
+
} else {
|
|
113
|
+
result.push(theme.fg("toolDiffContext", ` ${parsed.lineNum} ${replaceTabs(parsed.content)}`));
|
|
114
|
+
i++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return result.join("\n");
|
|
119
|
+
}
|