gentle-pi 3.7.0 → 4.0.0
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 +37 -6
- package/assets/agents/gentle-ai-explore.md +4 -4
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -9
- package/assets/orchestrator-delegation.md +40 -41
- package/assets/orchestrator-memory.md +1 -22
- package/assets/orchestrator-skills.md +1 -1
- package/assets/orchestrator.md +12 -24
- package/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/gentle-shell.mjs +254 -29
- package/docs/delegated-verification.md +26 -1
- package/docs/gentle-agents-activity.md +24 -0
- package/docs/gentle-shell.md +92 -28
- package/docs/native-authority-architecture.md +2 -2
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +112 -206
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +26 -0
- package/extensions/child-safety.ts +23 -0
- package/extensions/gentle-agents.ts +429 -262
- package/extensions/gentle-ai.ts +781 -683
- package/extensions/gentle-shell.ts +1375 -88
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +18 -7
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1419 -0
- package/extensions/history/load-shared-history.ts +39 -0
- package/extensions/history/selector-helpers.ts +538 -0
- package/extensions/history/session-scan.ts +233 -0
- package/extensions/history/store.ts +1119 -0
- package/extensions/nan-provider.ts +6 -0
- package/extensions/quiet-tools.ts +179 -87
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +16 -12
- package/extensions/startup-banner.ts +60 -31
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-protocol.ts +60 -0
- package/lib/agents-runner.ts +96 -98
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/bounded-writer-admission.ts +147 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/codemode-renderer.ts +185 -0
- package/lib/command-palette-catalog.ts +3 -9
- package/lib/command-palette.ts +25 -14
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-launcher.ts +24 -13
- package/lib/gentle-shell-resume-hint.ts +176 -0
- package/lib/history-capture-policy.ts +95 -0
- package/lib/model-routing-authority.ts +5 -1
- package/lib/nan-provider.ts +227 -0
- package/lib/native-review-cli.ts +49 -108
- package/lib/odd-phase-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- package/lib/overlay-repaint.ts +26 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +67 -17
- package/lib/review-candidate-view.ts +112 -25
- package/lib/review-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +156 -11
- package/lib/review-sidebar-state.ts +223 -0
- package/lib/selection-engine.ts +515 -0
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-worktree-registry.ts +14 -2
- package/lib/shell-bar.ts +179 -24
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +2 -1
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +78 -14
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +23 -13
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +66 -10
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +379 -0
- package/lib/vim-normal-engine.ts +154 -0
- package/lib/vim-operator-engine.ts +416 -0
- package/lib/vim-policy.ts +49 -0
- package/lib/vim-visual-engine.ts +107 -0
- package/lib/visual-customization-policy.ts +108 -0
- package/lib/visual-customize-view.ts +330 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/yolo-session-policy.ts +240 -0
- package/package.json +20 -8
- package/runtime/gentle-shell-launcher.mjs +23 -12
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- package/runtime/native-review-cli.mjs +49 -108
- package/runtime/review-risk-assessment.mjs +154 -9
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/gentle-ai-installer.mjs +14 -13
- package/scripts/mirror-odd-routing.mjs +2 -2
- package/scripts/run-test-suite.mjs +76 -0
- package/scripts/test-packed-runner.mjs +31 -14
- package/scripts/verify-package-files.mjs +11 -22
- package/skills/branch-pr/SKILL.md +24 -52
- package/skills/chained-pr/SKILL.md +31 -15
- package/skills/chained-pr/references/chaining-details.md +31 -20
- package/skills/gentle-ai/SKILL.md +9 -15
- package/skills/issue-creation/SKILL.md +8 -2
- package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
- package/skills/work-unit-commits/SKILL.md +4 -3
- package/tests/agent-profiles.test.ts +18 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +40 -0
- package/tests/agents-runner.test.ts +378 -89
- package/tests/agents-view-thread-identity.test.ts +169 -0
- package/tests/agents-view.test.ts +8 -2
- package/tests/agents-widget.test.ts +154 -15
- package/tests/append-system-prompt-route.test.ts +160 -0
- package/tests/append-system-prompt.test.ts +46 -0
- package/tests/artifact-language.test.ts +19 -213
- package/tests/ask-user-question.test.ts +44 -1
- package/tests/asset-installation-runtime.test.ts +5 -16
- package/tests/autonomous-guard.test.ts +69 -1
- package/tests/bounded-writer-admission.test.ts +95 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- package/tests/card-style-policy.test.ts +55 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-context-files.test.ts +255 -0
- package/tests/child-safety.test.ts +82 -0
- package/tests/codemode-rendering.test.ts +491 -0
- package/tests/command-palette.test.ts +39 -3
- package/tests/delegated-key-learnings-contract.test.ts +0 -76
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +170 -2
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
- package/tests/generic-agent-tools.test.ts +54 -0
- package/tests/gentle-agents.test.ts +1047 -252
- package/tests/gentle-ai-binary.test.ts +3 -3
- package/tests/gentle-ai-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +68 -54
- package/tests/gentle-ai-renderer.test.ts +487 -8
- package/tests/gentle-ai.test.ts +288 -65
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-shell-bin.test.ts +651 -114
- package/tests/gentle-shell-launcher.test.ts +99 -52
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- package/tests/gentle-shell.test.ts +3839 -187
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +80 -8
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-command-registration.test.ts +164 -0
- package/tests/history-dedupe-entries.test.ts +123 -0
- package/tests/history-delete-backfill.test.ts +190 -0
- package/tests/history-delete-confirm.test.ts +460 -0
- package/tests/history-dispatch.test.ts +180 -0
- package/tests/history-drain-hidden.test.ts +110 -0
- package/tests/history-drain-order.test.ts +98 -0
- package/tests/history-expanded-globals.test.ts +62 -0
- package/tests/history-gc.test.ts +832 -0
- package/tests/history-header-layout.test.ts +265 -0
- package/tests/history-hide-prompts.test.ts +275 -0
- package/tests/history-lazy-windowing.test.ts +508 -0
- package/tests/history-legacy-migrate-v2.test.ts +297 -0
- package/tests/history-load-shared-history.test.ts +53 -0
- package/tests/history-max-results-cap.test.ts +76 -0
- package/tests/history-multi-reader.test.ts +203 -0
- package/tests/history-off-path.test.ts +170 -0
- package/tests/history-openflow-integration.test.ts +173 -0
- package/tests/history-overlay-margin.test.ts +326 -0
- package/tests/history-preview-layout.test.ts +93 -0
- package/tests/history-registry.test.ts +143 -0
- package/tests/history-scope-delete.test.ts +411 -0
- package/tests/history-search-caret-keys.test.ts +142 -0
- package/tests/history-seed-bootstrap.test.ts +170 -0
- package/tests/history-seed-regen.test.ts +129 -0
- package/tests/history-selector-windowing.test.ts +94 -0
- package/tests/history-session-scan-directory.test.ts +87 -0
- package/tests/history-session-scan-extract.test.ts +583 -0
- package/tests/history-session-writer.test.ts +351 -0
- package/tests/history-store-paths.test.ts +79 -0
- package/tests/history-tombstone-exact.test.ts +139 -0
- package/tests/history-wheel-mouse.test.ts +242 -0
- package/tests/inprocess-reviewer.test.ts +29 -19
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/model-routing-authority.test.ts +16 -0
- package/tests/nan-provider.test.ts +471 -0
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-cli.test.ts +6 -120
- package/tests/native-review-parity-runtime.test.ts +100 -3
- package/tests/odd-integration.test.ts +67 -0
- package/tests/odd-phase-inference.test.ts +213 -0
- package/tests/odd-phase-loader.test.ts +253 -0
- package/tests/odd-phase.test.ts +307 -0
- package/tests/odd-routing-canonical-ratchet.test.ts +11 -6
- package/tests/odd-routing-contract.test.ts +86 -35
- package/tests/orchestrator-budget.test.ts +14 -39
- package/tests/orchestrator-rdd-ownership.test.ts +3 -3
- package/tests/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +252 -115
- package/tests/packed-runner-owned-path.test.ts +46 -0
- package/tests/persona-single-channel.test.ts +6 -6
- package/tests/provider-defect-handoff.test.ts +3 -11
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +409 -184
- package/tests/rdd-aware-verification-contract.test.ts +76 -1
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +176 -12
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +298 -0
- package/tests/review-contract-prompt.test.ts +108 -43
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-routing.test.ts +611 -5
- package/tests/review-controller-workspace-root.test.ts +163 -4
- package/tests/review-host-relay-routing.test.ts +338 -2
- package/tests/review-integration-v2-forward.test.ts +200 -0
- package/tests/review-ledger-contract.test.ts +10 -34
- package/tests/review-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +498 -6
- package/tests/review-sidebar-state.test.ts +402 -0
- package/tests/run-test-suite.test.ts +124 -0
- package/tests/runtime-harness.mjs +145 -786
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/selection-engine.test.ts +421 -0
- package/tests/session-messaging-grants.test.ts +255 -0
- package/tests/session-worktree-registry.test.ts +77 -0
- package/tests/shell-bar.test.ts +382 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +52 -0
- package/tests/shell-prompt.test.ts +94 -2
- package/tests/shell-sidebar-layout.test.ts +325 -21
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +87 -1
- package/tests/shell-usage-view.test.ts +27 -0
- package/tests/shell-usage.test.ts +73 -0
- package/tests/skill-registry.test.ts +50 -1
- package/tests/startup-banner.test.ts +130 -2
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -0
- package/tests/telemetry-trigger.test.ts +81 -20
- package/tests/theme-customization.test.ts +72 -0
- package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
- package/tests/vim-editor-adapter.test.ts +804 -0
- package/tests/vim-normal-engine.test.ts +101 -0
- package/tests/vim-operator-engine.test.ts +215 -0
- package/tests/vim-policy.test.ts +19 -0
- package/tests/vim-visual-engine.test.ts +52 -0
- package/tests/visual-customization-policy.test.ts +110 -0
- package/tests/visual-customize-view.test.ts +418 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/yolo-customize.test.ts +256 -0
- package/tests/yolo-mode-runtime.test.ts +161 -0
- package/tests/yolo-mode.test.ts +261 -0
- package/tests/yolo-session-policy.test.ts +59 -0
- package/themes/Gentle.json +2 -1
- package/themes/Gentleman-Cute.json +2 -1
- package/themes/Gentleman-Sexy.json +2 -1
- package/assets/agents/sdd-apply.md +0 -159
- package/assets/agents/sdd-archive.md +0 -228
- package/assets/agents/sdd-design.md +0 -49
- package/assets/agents/sdd-explore.md +0 -48
- package/assets/agents/sdd-init.md +0 -56
- package/assets/agents/sdd-onboard.md +0 -52
- package/assets/agents/sdd-proposal.md +0 -64
- package/assets/agents/sdd-remediate.md +0 -37
- package/assets/agents/sdd-research.md +0 -49
- package/assets/agents/sdd-spec.md +0 -192
- package/assets/agents/sdd-status.md +0 -54
- package/assets/agents/sdd-tasks.md +0 -108
- package/assets/agents/sdd-verify.md +0 -124
- package/assets/chains/sdd-full.chain.md +0 -83
- package/assets/chains/sdd-plan.chain.md +0 -56
- package/assets/chains/sdd-verify.chain.md +0 -43
- package/assets/sdd-orchestrator-workflow.md +0 -319
- package/assets/support/sdd-status-contract.md +0 -77
- package/docs/assets/diagrams/sdd-cycle.svg +0 -14
- package/extensions/sdd-init.ts +0 -816
- package/lib/openspec-deltas.ts +0 -156
- package/lib/sdd-preflight.ts +0 -1066
- package/lib/sdd-research-capabilities.ts +0 -94
- package/lib/sdd-status.ts +0 -26
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
- package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
- package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
- package/tests/openspec-deltas.test.ts +0 -209
- package/tests/sdd-agent-tools.test.ts +0 -156
- package/tests/sdd-archive-replay.test.ts +0 -82
- package/tests/sdd-classical-continuation.test.ts +0 -74
- package/tests/sdd-execution-routing-contract.test.ts +0 -44
- package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
- package/tests/sdd-native-managed-uptake.test.ts +0 -243
- package/tests/sdd-no-attempts-contract.test.ts +0 -15
- package/tests/sdd-odd-integration.test.ts +0 -33
- package/tests/sdd-optional-research.test.ts +0 -124
- package/tests/sdd-planning-routing-contract.test.ts +0 -45
- package/tests/sdd-preflight-rpc-input.test.ts +0 -125
- package/tests/sdd-preflight.test.ts +0 -541
- package/tests/sdd-research-capabilities.test.ts +0 -114
- package/tests/sdd-research-live.test.ts +0 -241
- package/tests/sdd-selection-transport.test.ts +0 -653
- package/tests/sdd-status.test.ts +0 -9
- package/tests/sdd-task-truth.test.ts +0 -43
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCodemodeExtension,
|
|
3
|
+
keyHint,
|
|
4
|
+
type AgentToolResult,
|
|
5
|
+
type CodemodeToolDetails,
|
|
6
|
+
type ExtensionAPI,
|
|
7
|
+
type ExtensionFactory,
|
|
8
|
+
type ToolDefinition,
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { getCapabilities, imageFallback, type Component } from "@earendil-works/pi-tui";
|
|
11
|
+
import {
|
|
12
|
+
CARD_TONE, cardAwaitingResult, cardBodyRows, cardBottom, cardLine, cardRunningLine, cardTop, floatRows, markCardResult,
|
|
13
|
+
type CardRowContext, type CardTheme, type CardTone,
|
|
14
|
+
} from "./shell-card.ts";
|
|
15
|
+
import { sanitizeTerminalText, stripAnsi } from "./terminal-theme.ts";
|
|
16
|
+
|
|
17
|
+
const CALL_STATUS = {
|
|
18
|
+
RUNNING: "running",
|
|
19
|
+
OK: "ok",
|
|
20
|
+
ERROR: "error",
|
|
21
|
+
CANCELLED: "cancelled",
|
|
22
|
+
} as const satisfies Record<string, CodemodeToolDetails["calls"][number]["status"]>;
|
|
23
|
+
const COLLAPSED_CALL_LIMIT = 8;
|
|
24
|
+
|
|
25
|
+
interface ObservedCall {
|
|
26
|
+
name?: string;
|
|
27
|
+
status?: CodemodeToolDetails["calls"][number]["status"];
|
|
28
|
+
durationMs?: number;
|
|
29
|
+
error?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function record(value: unknown): Record<string, unknown> {
|
|
33
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
34
|
+
? value as Record<string, unknown> : {};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function safe(value: string): string {
|
|
38
|
+
return sanitizeTerminalText(value)
|
|
39
|
+
.replace(/[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, "")
|
|
40
|
+
.replace(/\t/g, " ");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function singleLine(value: string): string {
|
|
44
|
+
return safe(value).replace(/[\r\n\t]/g, " ");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function observedCalls(details: unknown): ObservedCall[] {
|
|
48
|
+
const calls = record(details).calls;
|
|
49
|
+
if (!Array.isArray(calls)) return [];
|
|
50
|
+
return calls.map((value) => {
|
|
51
|
+
const call = record(value);
|
|
52
|
+
return {
|
|
53
|
+
name: typeof call.name === "string" ? singleLine(call.name) : undefined,
|
|
54
|
+
status: Object.values(CALL_STATUS).includes(call.status as ObservedCall["status"] & string)
|
|
55
|
+
? call.status as ObservedCall["status"] : undefined,
|
|
56
|
+
durationMs: typeof call.durationMs === "number" && Number.isFinite(call.durationMs) && call.durationMs >= 0
|
|
57
|
+
? call.durationMs : undefined,
|
|
58
|
+
error: typeof call.error === "string" ? safe(call.error) : undefined,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function duration(ms: number | undefined): string {
|
|
64
|
+
if (ms === undefined) return "";
|
|
65
|
+
return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function failed(call: ObservedCall): boolean {
|
|
69
|
+
return call.status === CALL_STATUS.ERROR || call.status === CALL_STATUS.CANCELLED || Boolean(call.error);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function childLine(call: ObservedCall, theme: CardTheme): string {
|
|
73
|
+
const status = call.status ?? "status unavailable";
|
|
74
|
+
const tone = failed(call) ? CARD_TONE.ERROR : call.status === CALL_STATUS.OK ? CARD_TONE.SUCCESS : CARD_TONE.WARNING;
|
|
75
|
+
const time = duration(call.durationMs);
|
|
76
|
+
// Arguments are deliberately never read; error payloads have their own bounded preview.
|
|
77
|
+
return `${theme.fg(tone, status)} · ${call.name || "name unavailable"}${time ? ` · ${time}` : ""}${call.error && call.status !== CALL_STATUS.ERROR ? " · error reported" : ""}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
class CodemodeCard implements Component {
|
|
81
|
+
private readonly theme: CardTheme;
|
|
82
|
+
private readonly tone: CardTone;
|
|
83
|
+
private readonly body: (width: number) => string[];
|
|
84
|
+
private readonly top: boolean;
|
|
85
|
+
private readonly hint?: string;
|
|
86
|
+
// Whether the call card above this result already drew body rows (the script).
|
|
87
|
+
private readonly afterBody: boolean;
|
|
88
|
+
// The call card's render context: until a result exists, the call closes the frame.
|
|
89
|
+
private readonly row?: CardRowContext;
|
|
90
|
+
|
|
91
|
+
constructor(theme: CardTheme, tone: CardTone, body: (width: number) => string[], top: boolean, hint?: string, afterBody = false, row?: CardRowContext) {
|
|
92
|
+
this.theme = theme;
|
|
93
|
+
this.tone = tone;
|
|
94
|
+
this.body = body;
|
|
95
|
+
this.top = top;
|
|
96
|
+
this.hint = hint;
|
|
97
|
+
this.afterBody = afterBody;
|
|
98
|
+
this.row = row;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
render(width: number): string[] {
|
|
102
|
+
const target = Math.max(0, Math.floor(width));
|
|
103
|
+
if (target === 0) return [];
|
|
104
|
+
const running = this.top && this.row !== undefined && cardAwaitingResult(this.row);
|
|
105
|
+
return floatRows(this.tone, this.theme, target, (inner) => ({
|
|
106
|
+
head: this.top ? [cardTop({ title: "Code", glyph: "λ", body: [], tone: this.tone }, this.theme, inner, this.hint)] : [],
|
|
107
|
+
body: running ? [...this.body(inner), cardRunningLine(this.tone, this.theme, inner)] : this.body(inner),
|
|
108
|
+
bottom: this.top && !running ? undefined : cardBottom(this.tone, this.theme, inner),
|
|
109
|
+
afterHeading: !this.top && !this.afterBody,
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
invalidate(): void {}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Replace presentation only: execute, schema and every loadout/exposure field retain their references. */
|
|
117
|
+
export function decorateCodemodeTool(tool: ToolDefinition): ToolDefinition {
|
|
118
|
+
return {
|
|
119
|
+
...tool,
|
|
120
|
+
renderShell: "self",
|
|
121
|
+
renderCall(args, theme, context) {
|
|
122
|
+
const tone = context.isError ? CARD_TONE.ERROR : context.isPartial ? CARD_TONE.WARNING : CARD_TONE.INFO;
|
|
123
|
+
const code = record(args).code;
|
|
124
|
+
const rows = context.expanded && typeof code === "string" ? [safe(code)] : [];
|
|
125
|
+
const hint = stripAnsi(keyHint("app.tools.expand", context.expanded ? "to collapse" : "to expand"));
|
|
126
|
+
return new CodemodeCard(theme, tone, (width) => cardBodyRows(rows, tone, theme, width, { expanded: true }), true, hint, false, context);
|
|
127
|
+
},
|
|
128
|
+
renderResult(result, options, theme, context) {
|
|
129
|
+
markCardResult(context?.state);
|
|
130
|
+
const calls = observedCalls(result.details);
|
|
131
|
+
const isError = context.isError || record(result).isError === true;
|
|
132
|
+
const failures = calls.filter(failed).length;
|
|
133
|
+
const tone = isError || failures > 0 ? CARD_TONE.ERROR : options.isPartial ? CARD_TONE.WARNING : CARD_TONE.INFO;
|
|
134
|
+
const shown = options.expanded ? calls : calls.slice(0, COLLAPSED_CALL_LIMIT);
|
|
135
|
+
const output = textOutput(result, context.showImages).flatMap((text) => {
|
|
136
|
+
const rows = text.split("\n");
|
|
137
|
+
// Pi prefixes final script output with status/wall-time bookkeeping.
|
|
138
|
+
// Keep it in expansion, but spend the collapsed budget on the payload.
|
|
139
|
+
const marker = rows.indexOf("Output:");
|
|
140
|
+
return !options.expanded && /^Script (?:completed|failed)/.test(rows[0] ?? "") && marker >= 0
|
|
141
|
+
? rows.slice(marker + 1) : rows;
|
|
142
|
+
});
|
|
143
|
+
const path = record(result.details).fullOutputPath;
|
|
144
|
+
// Mirrors renderCall: an expanded call shows the script as its body.
|
|
145
|
+
const callHasBody = options.expanded && typeof record(context?.args).code === "string";
|
|
146
|
+
return new CodemodeCard(theme, tone, (width) => {
|
|
147
|
+
const rows = shown.flatMap((call) => [
|
|
148
|
+
childLine(call, theme),
|
|
149
|
+
...(options.expanded && call.error ? [theme.fg("error", call.error)] : []),
|
|
150
|
+
]);
|
|
151
|
+
if (shown.length < calls.length) rows.push(theme.fg("muted", `${failures} errors/cancellations reported · ${calls.length - shown.length} more calls · expand to inspect`));
|
|
152
|
+
if (isError) rows.push(theme.fg("error", "Script failed"));
|
|
153
|
+
if (calls.length === 0) rows.push(theme.fg("muted", "No observed child calls"));
|
|
154
|
+
// Each child gets one collapsed physical row, preserving the observed order.
|
|
155
|
+
const body = options.expanded
|
|
156
|
+
? cardBodyRows(rows, tone, theme, width, { expanded: true })
|
|
157
|
+
: rows.map((row) => cardLine(row, tone, theme, width));
|
|
158
|
+
if (!options.expanded) {
|
|
159
|
+
const errors = calls.filter((call) => call.error).map((call) => `${call.name || "name unavailable"}: ${call.error}`);
|
|
160
|
+
body.push(...cardBodyRows(errors.map((error) => theme.fg("error", error)), tone, theme, width, { expanded: false, previewRows: 2 }));
|
|
161
|
+
}
|
|
162
|
+
body.push(...cardBodyRows(output.filter((row) => options.expanded || row.trim().length > 0).map((row) => theme.fg(isError ? "error" : "toolOutput", row)), tone, theme, width, { expanded: options.expanded, previewRows: 3 }));
|
|
163
|
+
if (typeof path === "string") body.push(...cardBodyRows([theme.fg("muted", `Full output: ${safe(path)}`)], tone, theme, width, { expanded: options.expanded, previewRows: 1 }));
|
|
164
|
+
return body;
|
|
165
|
+
}, false, undefined, callHasBody);
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function textOutput(result: AgentToolResult<unknown>, showImages: boolean): string[] {
|
|
171
|
+
const showImageFallback = !getCapabilities().images || !showImages;
|
|
172
|
+
return result.content.flatMap((item) => item.type === "text" ? [safe(item.text)]
|
|
173
|
+
: item.type === "image" && showImageFallback ? [imageFallback(safe(item.mimeType))] : []);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Run the public factory once, intercepting only its registration; all other API access passes through. */
|
|
177
|
+
export function registerCompactCodemode(pi: ExtensionAPI, factory: ExtensionFactory = createCodemodeExtension()): ReturnType<ExtensionFactory> {
|
|
178
|
+
const decoratedAPI = new Proxy(pi, {
|
|
179
|
+
get(target, property, receiver) {
|
|
180
|
+
if (property === "registerTool") return (tool: ToolDefinition) => pi.registerTool(tool.name === "codemode" ? decorateCodemodeTool(tool) : tool);
|
|
181
|
+
return Reflect.get(target, property, receiver);
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
return factory(decoratedAPI);
|
|
185
|
+
}
|
|
@@ -26,7 +26,9 @@ export const COMMAND_PALETTE_CATALOG: readonly CommandPaletteCatalogGroup[] = [
|
|
|
26
26
|
{ command: "gentle:review-mode", label: "Review mode (receipt-driven development)" },
|
|
27
27
|
{ command: "gentle:background-subagents", label: "Background subagents" },
|
|
28
28
|
{ command: "gentle:double-esc-cancel", label: "Require double Esc to cancel" },
|
|
29
|
+
{ command: "gentle:customize", label: "Visual customization" },
|
|
29
30
|
{ command: "gentle:animations", label: "Animation mode" },
|
|
31
|
+
{ command: "gentle:vim", label: "Vim opt-in · Pi slash commands" },
|
|
30
32
|
{ command: "gentle:telemetry", label: "Telemetry" },
|
|
31
33
|
{ command: "gentle:banner", label: "Startup banner" },
|
|
32
34
|
{ command: "gentle:banner-color", label: "Banner color" },
|
|
@@ -38,6 +40,7 @@ export const COMMAND_PALETTE_CATALOG: readonly CommandPaletteCatalogGroup[] = [
|
|
|
38
40
|
{
|
|
39
41
|
title: "Session",
|
|
40
42
|
items: [
|
|
43
|
+
{ command: "gentle:yolo", label: "🚀 YOLO 🔥 session permission" },
|
|
41
44
|
{ command: "gentle:changes", label: "Browse captured changes" },
|
|
42
45
|
{ command: "gentle:agents", label: "Subagents" },
|
|
43
46
|
{ command: "gentle:usage", label: "Subscription usage" },
|
|
@@ -51,15 +54,6 @@ export const COMMAND_PALETTE_CATALOG: readonly CommandPaletteCatalogGroup[] = [
|
|
|
51
54
|
{ command: "gentle:doctor", label: "Doctor" },
|
|
52
55
|
],
|
|
53
56
|
},
|
|
54
|
-
{
|
|
55
|
-
title: "SDD",
|
|
56
|
-
items: [
|
|
57
|
-
{ command: "gentle:sdd-preflight", label: "SDD preflight" },
|
|
58
|
-
{ command: "gentle-sdd-status", label: "SDD status" },
|
|
59
|
-
{ command: "gentle-sdd-continue", label: "SDD continue" },
|
|
60
|
-
{ command: "gentle-sdd-init", label: "SDD init" },
|
|
61
|
-
],
|
|
62
|
-
},
|
|
63
57
|
{
|
|
64
58
|
title: "Skills",
|
|
65
59
|
items: [{ command: "skill-registry:refresh", label: "Refresh skill registry" }],
|
package/lib/command-palette.ts
CHANGED
|
@@ -97,7 +97,30 @@ const TONE_COLOR: Record<Tone, string> = {
|
|
|
97
97
|
};
|
|
98
98
|
|
|
99
99
|
// Two border glyphs ("│ " and " │") plus the padding space on each side.
|
|
100
|
-
const CARD_PADDING = 4;
|
|
100
|
+
export const CARD_PADDING = 4;
|
|
101
|
+
|
|
102
|
+
/** Commands card chrome shared with other fullscreen overlay panels. */
|
|
103
|
+
export function renderPaletteCard(lines: string[], width: number, theme?: CommandPaletteTheme): string[] {
|
|
104
|
+
const innerWidth = Math.max(1, width - CARD_PADDING);
|
|
105
|
+
const border = (text: string) => theme ? theme.fg("border", text) : text;
|
|
106
|
+
const horizontal = "─".repeat(innerWidth + 2);
|
|
107
|
+
return [border(`╭${horizontal}╮`), ...lines.map((content) => {
|
|
108
|
+
const visible = visibleWidth(stripAnsi(content));
|
|
109
|
+
const fitted = visible > innerWidth ? truncateToWidth(content, innerWidth, "…", true) : content + " ".repeat(innerWidth - visible);
|
|
110
|
+
return `${border("│")} ${fitted} ${border("│")}`;
|
|
111
|
+
}), border(`╰${horizontal}╯`)];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function renderPaletteSelection(label: string, width: number, focused: boolean, theme?: CommandPaletteTheme): string {
|
|
115
|
+
const safe = sanitizeTerminalText(label);
|
|
116
|
+
if (focused && theme?.bg) {
|
|
117
|
+
const row = truncateToWidth(` ${safe}`, width, "…", true);
|
|
118
|
+
return theme.bg("selectedBg", row + " ".repeat(Math.max(0, width - visibleWidth(stripAnsi(row)))));
|
|
119
|
+
}
|
|
120
|
+
const prefix = focused ? (theme ? theme.fg("accent", "▸ ") : "▸ ") : " ";
|
|
121
|
+
const text = truncateToWidth(safe, Math.max(0, width - 2), "…", true);
|
|
122
|
+
return prefix + (theme ? theme.fg("text", text) : text);
|
|
123
|
+
}
|
|
101
124
|
const CARD_BORDER_ROWS = 2;
|
|
102
125
|
// Fixed content rows outside the grouped match list: header, query/search,
|
|
103
126
|
// a blank row, a blank row before the footer, and the footer itself.
|
|
@@ -194,7 +217,7 @@ export class CommandPalette {
|
|
|
194
217
|
render(width: number): string[] {
|
|
195
218
|
const innerWidth = Math.max(1, width - CARD_PADDING);
|
|
196
219
|
const maxTotalLines = Math.max(MIN_TOTAL_ROWS, Math.floor(this.rowsFn() * HEIGHT_RATIO));
|
|
197
|
-
return
|
|
220
|
+
return renderPaletteCard(this.renderBody(innerWidth, maxTotalLines), width, this.theme);
|
|
198
221
|
}
|
|
199
222
|
|
|
200
223
|
/**
|
|
@@ -307,18 +330,6 @@ export class CommandPalette {
|
|
|
307
330
|
return `${left}${" ".repeat(gap)}${right}${" ".repeat(trailingPad)}`;
|
|
308
331
|
}
|
|
309
332
|
|
|
310
|
-
private renderCard(lines: string[], innerWidth: number): string[] {
|
|
311
|
-
const horizontal = "─".repeat(innerWidth + 2);
|
|
312
|
-
const border = (text: string) => this.renderText(text, "border");
|
|
313
|
-
return [border(`╭${horizontal}╮`), ...lines.map((content) => `${border("│")} ${this.fitStyledLine(content, innerWidth)} ${border("│")}`), border(`╰${horizontal}╯`)];
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
private fitStyledLine(content: string, width: number): string {
|
|
317
|
-
const visible = visibleWidth(stripAnsi(content));
|
|
318
|
-
if (visible > width) return truncateToWidth(content, Math.max(1, width), "…", true);
|
|
319
|
-
return `${content}${" ".repeat(Math.max(0, width - visible))}`;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
333
|
private renderLine(text = "", width: number, tone?: Tone): string {
|
|
323
334
|
const safe = truncateToWidth(sanitizeTerminalText(text), Math.max(1, width), "…", true);
|
|
324
335
|
return tone ? this.renderText(safe, tone) : safe;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Recognizable executable forms only. This is deliberately not a shell/SQL
|
|
2
|
+
// interpreter or a sandbox: no expansion, script loading or remote inspection.
|
|
3
|
+
const DESTRUCTION_KIND = { DATABASE: "database", FILESYSTEM: "filesystem", GIT: "git" } as const;
|
|
4
|
+
type DestructionKind = (typeof DESTRUCTION_KIND)[keyof typeof DESTRUCTION_KIND];
|
|
5
|
+
export interface DestructiveCommandMatch {
|
|
6
|
+
kind: DestructionKind;
|
|
7
|
+
triggerIndex: number;
|
|
8
|
+
hardDeny: boolean;
|
|
9
|
+
}
|
|
10
|
+
interface Token {
|
|
11
|
+
value: string;
|
|
12
|
+
raw: string;
|
|
13
|
+
operator: boolean;
|
|
14
|
+
index: number;
|
|
15
|
+
}
|
|
16
|
+
const separators = new Set([";", "&&", "||", "|", "&", "\n", "(", ")"]);
|
|
17
|
+
|
|
18
|
+
/** Keep quoted arguments intact so harmless prose is never treated as a command. */
|
|
19
|
+
function tokenize(command: string): Token[] {
|
|
20
|
+
const tokens: Token[] = [];
|
|
21
|
+
// Literal, named heredocs are data, not executable shell segments.
|
|
22
|
+
command = command.replace(/<<-?\s*(['"]?)([A-Za-z_]\w*)\1[^\n]*\n[\s\S]*?\n\2(?=\n|$)/g, (raw) => {
|
|
23
|
+
const headerLength = raw.indexOf("\n") + 1;
|
|
24
|
+
return raw.slice(0, headerLength) + " ".repeat(raw.length - headerLength);
|
|
25
|
+
});
|
|
26
|
+
const pattern = /(?:"(?:\\.|[^"\\])*"|'[^']*'|\\.|[^\s;&|()'"\\])+|&&|\|\||[;&|()\n]/g;
|
|
27
|
+
for (const match of command.matchAll(pattern)) {
|
|
28
|
+
const raw = match[0];
|
|
29
|
+
const value = raw.replace(/"((?:\\.|[^"\\])*)"|'([^']*)'|\\(.)/g,
|
|
30
|
+
(_raw, double: string | undefined, single: string | undefined, escaped: string | undefined) => {
|
|
31
|
+
if (double !== undefined) return double.replace(/\\(["\\$`\n])/g, (_escape, char: string) => char === "\n" ? "" : char);
|
|
32
|
+
return single ?? escaped ?? "";
|
|
33
|
+
});
|
|
34
|
+
// Only raw operators are boundaries; quoted/escaped operators are words.
|
|
35
|
+
tokens.push({ value, raw, operator: separators.has(raw), index: match.index });
|
|
36
|
+
}
|
|
37
|
+
return tokens;
|
|
38
|
+
}
|
|
39
|
+
const executable = (value: string) => value.split("/").at(-1) ?? value;
|
|
40
|
+
|
|
41
|
+
function sqlDestroys(sql: string): boolean {
|
|
42
|
+
// Mask quoted identifiers as non-keyword atoms, and remove comments and
|
|
43
|
+
// strings. A table named "where" is not a predicate or a statement boundary.
|
|
44
|
+
const code = sql.replace(/--[^\n]*|\/\*[\s\S]*?\*\/|'(?:''|[^'])*'|"(?:""|[^"])*"|`(?:``|[^`])*`|\[(?:\]\]|[^\]])*\]/g,
|
|
45
|
+
(raw) => /^["`\[]/.test(raw) ? "identifier" : " ");
|
|
46
|
+
return code.split(";").some((statement) => {
|
|
47
|
+
if (/^\s*(?:DROP\s+(?:TABLE|DATABASE|SCHEMA|INDEX|VIEW)\b|TRUNCATE\s+(?:TABLE\s+)?\S)/i.test(statement)) return true;
|
|
48
|
+
if (!/^\s*DELETE\s+FROM\s+\S/i.test(statement)) return false;
|
|
49
|
+
return !/\bWHERE\b/i.test(statement) || /\bWHERE\s+(?:1\s*=\s*1|TRUE)\s*$/i.test(statement.trim());
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function recognizeDestructiveCommands(command: string, depth = 0): DestructiveCommandMatch[] {
|
|
54
|
+
if (depth > 4) return [];
|
|
55
|
+
const tokens = tokenize(command);
|
|
56
|
+
const matches: DestructiveCommandMatch[] = [];
|
|
57
|
+
let previous: Token[] = [];
|
|
58
|
+
let previousSeparator = "";
|
|
59
|
+
let start = 0;
|
|
60
|
+
for (let end = 0; end <= tokens.length; end++) {
|
|
61
|
+
if (end < tokens.length && !tokens[end].operator) continue;
|
|
62
|
+
const segment = tokens.slice(start, end);
|
|
63
|
+
let cursor = 0;
|
|
64
|
+
// Assignments and familiar executable wrappers preserve command position.
|
|
65
|
+
while (cursor < segment.length) {
|
|
66
|
+
const value = segment[cursor].value;
|
|
67
|
+
if (/^[A-Za-z_][A-Za-z_0-9]*=/.test(value)) {
|
|
68
|
+
cursor++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const name = executable(value);
|
|
72
|
+
if (!["env", "sudo", "command", "exec", "nohup", "timeout", "xargs"].includes(name)) break;
|
|
73
|
+
const valueOptions: Record<string, readonly string[]> = {
|
|
74
|
+
env: ["-u", "--unset", "-C", "--chdir"],
|
|
75
|
+
sudo: ["-u", "-g", "-h", "-p", "-C", "--user", "--group", "--host", "--prompt", "--chdir"],
|
|
76
|
+
timeout: ["-s", "--signal", "-k", "--kill-after"],
|
|
77
|
+
xargs: ["-n", "-I", "-P", "--max-args", "--replace", "--max-procs"],
|
|
78
|
+
};
|
|
79
|
+
cursor++;
|
|
80
|
+
while (cursor < segment.length && segment[cursor].value.startsWith("-")) {
|
|
81
|
+
const flag = segment[cursor++].value;
|
|
82
|
+
if (flag === "--") break;
|
|
83
|
+
if (valueOptions[name]?.includes(flag)) cursor++;
|
|
84
|
+
}
|
|
85
|
+
if (name === "timeout") cursor++;
|
|
86
|
+
}
|
|
87
|
+
const head = segment[cursor];
|
|
88
|
+
const name = executable(head?.value ?? "");
|
|
89
|
+
const args = segment.slice(cursor + 1).map((token) => token.value);
|
|
90
|
+
const add = (kind: DestructionKind, hardDeny = false) => matches.push({ kind, hardDeny, triggerIndex: head.index });
|
|
91
|
+
if (["sh", "bash", "zsh", "dash"].includes(name)) {
|
|
92
|
+
const flag = args.findIndex((arg) => /^-[a-z]*c[a-z]*$/.test(arg));
|
|
93
|
+
if (flag >= 0 && args[flag + 1]) {
|
|
94
|
+
// Nested offsets use the enclosing executable, which is stable even
|
|
95
|
+
// when quote removal changes the payload's byte positions.
|
|
96
|
+
matches.push(...recognizeDestructiveCommands(args[flag + 1], depth + 1).map((match) => ({ ...match, triggerIndex: head.index })));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (["psql", "mysql", "mariadb", "sqlite3"].includes(name)) {
|
|
100
|
+
const payloads = args.map((arg) => arg.replace(/^(?:--command|--execute)=/, ""));
|
|
101
|
+
const heredoc = command.slice(head.index).match(/^[^\n]*<<-?\s*(['"]?)([A-Za-z_]\w*)\1[^\n]*\n([\s\S]*?)\n\2(?=\n|$)/);
|
|
102
|
+
if (heredoc) payloads.push(heredoc[3]);
|
|
103
|
+
if (previousSeparator === "|" && ["echo", "printf"].includes(executable(previous[0]?.value ?? ""))) {
|
|
104
|
+
payloads.push(previous.slice(1).map((token) => token.value).join(" ").replace(/^%s\s*/, ""));
|
|
105
|
+
}
|
|
106
|
+
if (payloads.some(sqlDestroys)) add(DESTRUCTION_KIND.DATABASE);
|
|
107
|
+
}
|
|
108
|
+
if (name === "rm" && args.some((arg) => /^-[^-]*[rR]/.test(arg) || arg === "--recursive")) {
|
|
109
|
+
const rootTarget = args.some((arg) => /^(?:\/(?:\*+)?$|\.{1,2}\/?$|~(?:\/|$)|\$\{?HOME\}?(?:\/|$))/.test(arg));
|
|
110
|
+
add(DESTRUCTION_KIND.FILESYSTEM, rootTarget);
|
|
111
|
+
}
|
|
112
|
+
if (name === "find") {
|
|
113
|
+
if (args.includes("-delete")) add(DESTRUCTION_KIND.FILESYSTEM);
|
|
114
|
+
for (let execIndex = 0; execIndex < args.length; execIndex++) {
|
|
115
|
+
if (!["-exec", "-execdir"].includes(args[execIndex])) continue;
|
|
116
|
+
const invocation = segment.slice(cursor + execIndex + 2);
|
|
117
|
+
const terminator = invocation.findIndex((arg) => arg.value === ";" || arg.value === "+");
|
|
118
|
+
const nested = (terminator < 0 ? invocation : invocation.slice(0, terminator)).map((arg) => arg.raw).join(" ");
|
|
119
|
+
matches.push(...recognizeDestructiveCommands(nested, depth + 1).map((match) => ({ ...match, triggerIndex: head.index })));
|
|
120
|
+
if (terminator < 0) break;
|
|
121
|
+
execIndex += terminator + 1;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (name === "git") {
|
|
125
|
+
let index = 0;
|
|
126
|
+
while (args[index]?.startsWith("-")) {
|
|
127
|
+
const flag = args[index++];
|
|
128
|
+
if (["-C", "-c", "--git-dir", "--work-tree", "--namespace"].includes(flag)) index++;
|
|
129
|
+
}
|
|
130
|
+
const action = args[index];
|
|
131
|
+
const flags = args.slice(index + 1);
|
|
132
|
+
const force = flags.some((arg) => /^--force(?:-with-lease|=|$)/.test(arg) || /^-[^-]*f/.test(arg));
|
|
133
|
+
const hard = (action === "reset" && flags.includes("--hard")) || (action === "clean" && force) || (action === "push" && force);
|
|
134
|
+
const branchDelete = action === "branch" && (flags.some((arg) => /^-[^-]*D/.test(arg)) || (force && flags.some((arg) => arg === "--delete" || /^-[^-]*d/.test(arg))));
|
|
135
|
+
if (hard || branchDelete || ["reset", "clean", "restore", "rebase"].includes(action) ||
|
|
136
|
+
(action === "checkout" && (force || flags.includes("--"))) ||
|
|
137
|
+
(action === "stash" && ["drop", "clear"].includes(flags[0]))) add(DESTRUCTION_KIND.GIT, hard);
|
|
138
|
+
}
|
|
139
|
+
previous = segment;
|
|
140
|
+
previousSeparator = tokens[end]?.value ?? "";
|
|
141
|
+
start = end + 1;
|
|
142
|
+
}
|
|
143
|
+
return matches;
|
|
144
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export const GENTLE_AI_TIMING_ENTRY = "gentle-ai-elapsed-timing/v1";
|
|
2
|
+
|
|
3
|
+
export interface GentleAiTimingRecord {
|
|
4
|
+
toolCallId: string;
|
|
5
|
+
startedAt: number;
|
|
6
|
+
endedAt?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** What the renderer consumes: a read-only lookup keyed by pi's stable toolCallId. */
|
|
10
|
+
export interface GentleAiTimingLookup {
|
|
11
|
+
lookup(toolCallId: string): GentleAiTimingRecord | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface GentleAiTimingEntry {
|
|
15
|
+
type: string;
|
|
16
|
+
customType?: string;
|
|
17
|
+
data?: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface GentleAiTimingSessionReader {
|
|
21
|
+
getEntries(): readonly GentleAiTimingEntry[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface GentleAiTimingAppendHost {
|
|
25
|
+
appendEntry(type: string, data?: unknown): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseGentleAiTimingData(data: unknown): GentleAiTimingRecord | undefined {
|
|
29
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return undefined;
|
|
30
|
+
const record = data as { toolCallId?: unknown; startedAt?: unknown; endedAt?: unknown };
|
|
31
|
+
if (typeof record.toolCallId !== "string" || record.toolCallId.length === 0) return undefined;
|
|
32
|
+
if (typeof record.startedAt !== "number" || !Number.isFinite(record.startedAt)) return undefined;
|
|
33
|
+
// Local capture + a plain typeof guard: under non-strict null checks a
|
|
34
|
+
// `x === undefined` condition leaves `unknown` unnarrowed, so the finite-
|
|
35
|
+
// number branch must be decided by typeof before the value is used.
|
|
36
|
+
const endedAt = record.endedAt;
|
|
37
|
+
if (endedAt !== undefined) {
|
|
38
|
+
if (typeof endedAt !== "number" || !Number.isFinite(endedAt)) return undefined;
|
|
39
|
+
return { toolCallId: record.toolCallId, startedAt: record.startedAt, endedAt };
|
|
40
|
+
}
|
|
41
|
+
return { toolCallId: record.toolCallId, startedAt: record.startedAt };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Replay: entries are append-only, so the last timing entry per toolCallId wins. */
|
|
45
|
+
export function readGentleAiTimings(entries: readonly GentleAiTimingEntry[]): Map<string, GentleAiTimingRecord> {
|
|
46
|
+
const timings = new Map<string, GentleAiTimingRecord>();
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
if (entry.type !== "custom" || entry.customType !== GENTLE_AI_TIMING_ENTRY) continue;
|
|
49
|
+
const record = parseGentleAiTimingData(entry.data);
|
|
50
|
+
if (record) timings.set(record.toolCallId, record);
|
|
51
|
+
}
|
|
52
|
+
return timings;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// No global store: the owning extension binds its own Pi API, and reading all
|
|
56
|
+
// entries also catches a launch that happened while the shell was absent.
|
|
57
|
+
export class GentleAiElapsedTimingLedger implements GentleAiTimingLookup {
|
|
58
|
+
private readonly recorded = new Set<string>();
|
|
59
|
+
private readonly session: GentleAiTimingSessionReader;
|
|
60
|
+
private readonly host?: GentleAiTimingAppendHost;
|
|
61
|
+
|
|
62
|
+
constructor(session: GentleAiTimingSessionReader, host?: GentleAiTimingAppendHost) {
|
|
63
|
+
this.session = session;
|
|
64
|
+
this.host = host;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
lookup(toolCallId: string): GentleAiTimingRecord | undefined {
|
|
68
|
+
return readGentleAiTimings(this.session.getEntries()).get(toolCallId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
recordStart(toolCallId: string, startedAt: number): void {
|
|
72
|
+
this.append(`${toolCallId}:start`, { toolCallId, startedAt });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
recordEnd(toolCallId: string, endedAt: number): void {
|
|
76
|
+
const startedAt = this.lookup(toolCallId)?.startedAt;
|
|
77
|
+
// A terminal without a durable start has no honest duration to freeze.
|
|
78
|
+
if (startedAt === undefined) return;
|
|
79
|
+
this.append(`${toolCallId}:end`, { toolCallId, startedAt, endedAt });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private append(stage: string, data: GentleAiTimingRecord): void {
|
|
83
|
+
if (this.recorded.has(stage)) return;
|
|
84
|
+
this.host?.appendEntry(GENTLE_AI_TIMING_ENTRY, data);
|
|
85
|
+
this.recorded.add(stage);
|
|
86
|
+
}
|
|
87
|
+
}
|