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
package/lib/shell-card.ts
CHANGED
|
@@ -23,12 +23,45 @@ export interface Card {
|
|
|
23
23
|
|
|
24
24
|
export interface CardTheme {
|
|
25
25
|
fg(color: string, text: string): string;
|
|
26
|
+
/** Pi themes have it; the float style needs it to paint its panel. */
|
|
27
|
+
bg?(color: string, text: string): string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Conversation cards come in two styles, chosen in /gentle:customize. Pi loads
|
|
31
|
+
// every extension with its own jiti loader (moduleCache:false), so each one
|
|
32
|
+
// gets a copy of this module; the global symbol keeps one style per process.
|
|
33
|
+
export const CARD_STYLE = {
|
|
34
|
+
NEON: "neon",
|
|
35
|
+
FLOAT: "float",
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
export type CardStyle = (typeof CARD_STYLE)[keyof typeof CARD_STYLE];
|
|
39
|
+
|
|
40
|
+
const CARD_STYLE_SLOT = Symbol.for("gentle-pi.card-style");
|
|
41
|
+
const styleState = globalThis as typeof globalThis & { [CARD_STYLE_SLOT]?: unknown };
|
|
42
|
+
|
|
43
|
+
export function cardStyle(): CardStyle {
|
|
44
|
+
return styleState[CARD_STYLE_SLOT] === CARD_STYLE.NEON ? CARD_STYLE.NEON : CARD_STYLE.FLOAT;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function setCardStyle(style: CardStyle): void {
|
|
48
|
+
styleState[CARD_STYLE_SLOT] = style;
|
|
26
49
|
}
|
|
27
50
|
|
|
28
51
|
export interface CardRenderOptions {
|
|
29
52
|
expanded: boolean;
|
|
53
|
+
/** Optional physical-row budget for useful tool previews; notices default to one. */
|
|
54
|
+
previewRows?: number;
|
|
30
55
|
/** Right-aligned hint in the top rule, e.g. the expand key. May carry ANSI. */
|
|
31
56
|
hint?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Fixed chrome panels (Agents, Todos, Status rail) opt in here. The float
|
|
59
|
+
* style then paints them like float cards, centered between two padding
|
|
60
|
+
* rows with a blank row between header and body: `panelExtraRows` taller
|
|
61
|
+
* than neon when a body exists, with the header on `panelHeaderRow`.
|
|
62
|
+
* Neon keeps the outlined frame. Conversation cards never set it.
|
|
63
|
+
*/
|
|
64
|
+
panel?: boolean;
|
|
32
65
|
}
|
|
33
66
|
|
|
34
67
|
export const CARD_GLYPH = "✿";
|
|
@@ -54,8 +87,39 @@ const BODY_ROLE = "text";
|
|
|
54
87
|
const SEPARATOR = "·";
|
|
55
88
|
const FRAME_COLUMNS = 4;
|
|
56
89
|
|
|
90
|
+
// The characters that draw a card around its content. The float style keeps
|
|
91
|
+
// every row the same width: the left edge becomes an accent bar and the rules
|
|
92
|
+
// and the right rail become spaces. Rows are built from these by position, so
|
|
93
|
+
// content that happens to contain box-drawing characters is never rewritten.
|
|
94
|
+
interface CardChrome {
|
|
95
|
+
topLeft: string;
|
|
96
|
+
topLead: string;
|
|
97
|
+
topRight: string;
|
|
98
|
+
rule: string;
|
|
99
|
+
side: string;
|
|
100
|
+
rail: string;
|
|
101
|
+
bottomLeft: string;
|
|
102
|
+
bottomRight: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const OUTLINE_CHROME: CardChrome = { topLeft: "╭", topLead: "─ ", topRight: "╮", rule: "─", side: "│", rail: "│", bottomLeft: "╰", bottomRight: "╯" };
|
|
106
|
+
// `▎ ` is one cell narrower than `╭─ `: the spare cell moves to the right end
|
|
107
|
+
// so the heading glyph starts in the same column as the body text.
|
|
108
|
+
const FLOAT_CHROME: CardChrome = { topLeft: "▎", topLead: " ", topRight: " ", rule: " ", side: "▎", rail: " ", bottomLeft: "▎", bottomRight: " " };
|
|
109
|
+
let chrome = OUTLINE_CHROME;
|
|
110
|
+
|
|
111
|
+
function withChrome<T>(next: CardChrome, run: () => T): T {
|
|
112
|
+
const previous = chrome;
|
|
113
|
+
chrome = next;
|
|
114
|
+
try {
|
|
115
|
+
return run();
|
|
116
|
+
} finally {
|
|
117
|
+
chrome = previous;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
57
121
|
function rule(length: number): string {
|
|
58
|
-
return
|
|
122
|
+
return chrome.rule.repeat(Math.max(0, length));
|
|
59
123
|
}
|
|
60
124
|
|
|
61
125
|
function titleText(card: Card, theme: CardTheme): { styled: string; width: number } {
|
|
@@ -92,13 +156,13 @@ export function cardTop(card: Card, theme: CardTheme, width: number, hint?: stri
|
|
|
92
156
|
const styledTitleWidth = title.width <= titleWidth ? title.width : visibleWidth(styledTitle);
|
|
93
157
|
const fill = rule(targetWidth - styledTitleWidth - 5 - hintWidth);
|
|
94
158
|
const tail = shownHint ? ` ${theme.fg(HINT_ROLE, shownHint)} ` : "";
|
|
95
|
-
return theme.fg(FRAME_ROLE[card.tone],
|
|
159
|
+
return theme.fg(FRAME_ROLE[card.tone], chrome.topLeft) + frame(theme, card.tone, chrome.topLead) + styledTitle + frame(theme, card.tone, ` ${fill}`) + tail + frame(theme, card.tone, chrome.topRight);
|
|
96
160
|
}
|
|
97
161
|
|
|
98
162
|
export function cardLine(text: string, tone: CardTone, theme: CardTheme, width: number): string {
|
|
99
163
|
const targetWidth = Math.max(0, Math.floor(width));
|
|
100
164
|
if (targetWidth === 0) return "";
|
|
101
|
-
const left = theme.fg(FRAME_ROLE[tone],
|
|
165
|
+
const left = theme.fg(FRAME_ROLE[tone], chrome.side);
|
|
102
166
|
if (targetWidth === 1) return left;
|
|
103
167
|
if (targetWidth === 2) return left + frame(theme, tone, "│");
|
|
104
168
|
if (targetWidth === 3) return `${left} ${frame(theme, tone, "│")}`;
|
|
@@ -106,34 +170,239 @@ export function cardLine(text: string, tone: CardTone, theme: CardTheme, width:
|
|
|
106
170
|
const innerWidth = targetWidth - FRAME_COLUMNS;
|
|
107
171
|
const clipped = innerWidth === 0 ? "" : truncateToWidth(text, innerWidth, "…");
|
|
108
172
|
const padding = " ".repeat(Math.max(0, innerWidth - visibleWidth(clipped)));
|
|
109
|
-
return `${left} ${clipped}${padding} ${frame(theme, tone,
|
|
173
|
+
return `${left} ${clipped}${padding} ${frame(theme, tone, chrome.rail)}`;
|
|
110
174
|
}
|
|
111
175
|
|
|
112
|
-
export function cardBottom(tone: CardTone, theme: CardTheme, width: number): string {
|
|
176
|
+
export function cardBottom(tone: CardTone, theme: CardTheme, width: number, content?: string): string {
|
|
113
177
|
const targetWidth = Math.max(0, Math.floor(width));
|
|
114
178
|
if (targetWidth === 0) return "";
|
|
115
|
-
const left = theme.fg(FRAME_ROLE[tone],
|
|
179
|
+
const left = theme.fg(FRAME_ROLE[tone], chrome.bottomLeft);
|
|
116
180
|
if (targetWidth === 1) return left;
|
|
117
|
-
return left + frame(theme, tone, `${rule(targetWidth - 2)}
|
|
181
|
+
if (content === undefined || visibleWidth(content) === 0) return left + frame(theme, tone, `${rule(targetWidth - 2)}${chrome.bottomRight}`);
|
|
182
|
+
const contentWidth = visibleWidth(content) + 2;
|
|
183
|
+
// Responsive: the duration rides the closing rule only when it fits with a minimum fill.
|
|
184
|
+
if (targetWidth - 2 - contentWidth < 3) return left + frame(theme, tone, `${rule(targetWidth - 2)}${chrome.bottomRight}`);
|
|
185
|
+
return left + frame(theme, tone, `${rule(targetWidth - 2 - contentWidth)} `) + theme.fg(HINT_ROLE, content) + frame(theme, tone, ` ${chrome.bottomRight}`);
|
|
118
186
|
}
|
|
119
187
|
|
|
120
188
|
export function cardInnerWidth(width: number): number {
|
|
121
189
|
return Math.max(1, width - FRAME_COLUMNS);
|
|
122
190
|
}
|
|
123
191
|
|
|
192
|
+
/** Shared body for whole cards and separate call/result components. Budgets apply after wrapping. */
|
|
193
|
+
export function cardBodyRows(
|
|
194
|
+
rows: readonly string[], tone: CardTone, theme: CardTheme, width: number,
|
|
195
|
+
options: CardRenderOptions,
|
|
196
|
+
): string[] {
|
|
197
|
+
if (width <= 0) return [];
|
|
198
|
+
const wrapped = rows.flatMap((row) => row.split("\n").flatMap((line) => line === "" ? [""] : wrapTextWithAnsi(line, cardInnerWidth(width))));
|
|
199
|
+
const limit = Math.max(0, Math.floor(options.previewRows ?? 3));
|
|
200
|
+
const shown = options.expanded ? wrapped : wrapped.slice(0, limit);
|
|
201
|
+
return shown.map((line) => cardLine(line, tone, theme, width));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Pi draws a tool row as the call component followed, once a result exists,
|
|
205
|
+
// by the result component, which closes the frame. The call cannot see the
|
|
206
|
+
// result, so result renderers mark the row state both share, and a call still
|
|
207
|
+
// waiting for its first result closes its own frame.
|
|
208
|
+
const RESULT_MARK = Symbol.for("gentle-pi.card-result");
|
|
209
|
+
const RUNNING_TEXT = "running…";
|
|
210
|
+
const RUNNING_ROLE = "muted";
|
|
211
|
+
|
|
212
|
+
/** The fields of pi's tool render context that tell whether a result exists. */
|
|
213
|
+
export interface CardRowContext {
|
|
214
|
+
isPartial?: boolean;
|
|
215
|
+
state?: unknown;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Result renderers mark the shared row state: from now on a result component closes the card. */
|
|
219
|
+
export function markCardResult(state: unknown): void {
|
|
220
|
+
if (state !== null && typeof state === "object") (state as Record<symbol, unknown>)[RESULT_MARK] = true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Whether a call card must close its own frame. Read it at render time: pi builds the result component after the call. */
|
|
224
|
+
export function cardAwaitingResult(context: CardRowContext): boolean {
|
|
225
|
+
if (context.isPartial === false) return false;
|
|
226
|
+
const state = context.state;
|
|
227
|
+
return !(state !== null && typeof state === "object" && (state as Record<symbol, unknown>)[RESULT_MARK] === true);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** The muted body row of a call card that is still waiting for its result. */
|
|
231
|
+
export function cardRunningLine(tone: CardTone, theme: CardTheme, width: number): string {
|
|
232
|
+
return cardLine(theme.fg(RUNNING_ROLE, RUNNING_TEXT), tone, theme, width);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Quiet calls may continue long or multiline headings inside the same frame. */
|
|
236
|
+
export function cardTopRows(card: Card, theme: CardTheme, width: number, hint?: string): string[] {
|
|
237
|
+
const target = Math.max(0, Math.floor(width));
|
|
238
|
+
if (target === 0) return [];
|
|
239
|
+
if (target < 8) return [cardTop(card, theme, target, hint)];
|
|
240
|
+
const [first = "", ...rest] = card.title.split("\n");
|
|
241
|
+
// Reserve the hint before wrapping, plus one rule column and enough heading
|
|
242
|
+
// space to retain identity. This policy belongs only to continuing tool tops.
|
|
243
|
+
const available = target - 6 - visibleWidth(`${card.glyph ?? CARD_GLYPH} `) - (card.subtitle ? visibleWidth(card.subtitle) + 3 : 0);
|
|
244
|
+
const hintWidth = hint ? visibleWidth(hint) + 2 : 0;
|
|
245
|
+
const shownHint = hint && available - hintWidth >= 12 ? hint : undefined;
|
|
246
|
+
const room = Math.max(1, available - (shownHint ? hintWidth : 0));
|
|
247
|
+
const [head = "", ...overflow] = wrapTextWithAnsi(first, room);
|
|
248
|
+
return [
|
|
249
|
+
cardTop({ ...card, title: head }, theme, target, shownHint),
|
|
250
|
+
...cardBodyRows([...overflow, ...rest], card.tone, theme, target, { expanded: true }),
|
|
251
|
+
];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** The rows one card component draws, by position. */
|
|
255
|
+
export interface CardParts {
|
|
256
|
+
/** The top rule plus any heading continuation rows. */
|
|
257
|
+
head?: readonly string[];
|
|
258
|
+
body?: readonly string[];
|
|
259
|
+
/** The closing rule, when this component closes the card. */
|
|
260
|
+
bottom?: string;
|
|
261
|
+
/** A component above this one already drew the heading. */
|
|
262
|
+
afterHeading?: boolean;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const FLOAT_MARGIN = 1;
|
|
266
|
+
const FLOAT_MIN_WIDTH = 10;
|
|
267
|
+
const FLOAT_BG_ROLE: Record<CardTone, string> = {
|
|
268
|
+
[CARD_TONE.INFO]: "toolSuccessBg",
|
|
269
|
+
[CARD_TONE.SUCCESS]: "toolSuccessBg",
|
|
270
|
+
[CARD_TONE.WARNING]: "toolPendingBg",
|
|
271
|
+
[CARD_TONE.ERROR]: "toolErrorBg",
|
|
272
|
+
};
|
|
273
|
+
const BG_RESET = "\x1b[49m";
|
|
274
|
+
// Full resets (pi-tui truncation inserts one) and background resets.
|
|
275
|
+
const BG_CLEARING = /\x1b\[(?:0|49)?m/g;
|
|
276
|
+
|
|
277
|
+
function panelOpener(theme: CardTheme, tone: CardTone): string {
|
|
278
|
+
if (typeof theme.bg !== "function") return "";
|
|
279
|
+
try {
|
|
280
|
+
const painted = theme.bg(FLOAT_BG_ROLE[tone], "");
|
|
281
|
+
return painted.endsWith(BG_RESET) ? painted.slice(0, -BG_RESET.length) : "";
|
|
282
|
+
} catch {
|
|
283
|
+
// A theme without the tool background keeps the outlined card.
|
|
284
|
+
return "";
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Draws one conversation card component in the active style. The outlined
|
|
290
|
+
* style returns the parts unchanged; the float style renders them one margin
|
|
291
|
+
* narrower with the float chrome and paints a tone background behind every
|
|
292
|
+
* row. A blank row sits above the heading and between it and the body.
|
|
293
|
+
*/
|
|
294
|
+
export function floatRows(tone: CardTone, theme: CardTheme, width: number, render: (width: number) => CardParts): string[] {
|
|
295
|
+
const target = Math.max(0, Math.floor(width));
|
|
296
|
+
const open = floatOpener(theme, tone, target);
|
|
297
|
+
if (!open) {
|
|
298
|
+
const { head = [], body = [], bottom } = render(width);
|
|
299
|
+
return [...head, ...body, ...(bottom === undefined ? [] : [bottom])];
|
|
300
|
+
}
|
|
301
|
+
const inner = target - FLOAT_MARGIN * 2;
|
|
302
|
+
const rows = withChrome(FLOAT_CHROME, () => {
|
|
303
|
+
const { head = [], body = [], bottom, afterHeading = false } = render(inner);
|
|
304
|
+
const blank = cardBottom(tone, theme, inner);
|
|
305
|
+
return [
|
|
306
|
+
...(head.length > 0 ? [blank, ...head] : []),
|
|
307
|
+
...(body.length > 0 && (head.length > 0 || afterHeading) ? [blank] : []),
|
|
308
|
+
...body,
|
|
309
|
+
...(bottom === undefined ? [] : [bottom]),
|
|
310
|
+
];
|
|
311
|
+
});
|
|
312
|
+
return paintFloat(rows, open);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** The tone background opener when the float style applies at this width, or "" for the outlined card. */
|
|
316
|
+
function floatOpener(theme: CardTheme, tone: CardTone, width: number): string {
|
|
317
|
+
return cardStyle() === CARD_STYLE.FLOAT && Math.floor(width) >= FLOAT_MIN_WIDTH ? panelOpener(theme, tone) : "";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Paints every row behind its tone background, re-armed after any reset the
|
|
321
|
+
// content carries, inside a transparent one-column margin on both sides.
|
|
322
|
+
// Padding rows keep their tone-coloured accent just like content rows.
|
|
323
|
+
function paintFloat(rows: readonly string[], open: string): string[] {
|
|
324
|
+
const margin = " ".repeat(FLOAT_MARGIN);
|
|
325
|
+
return rows.map((row) => `${margin}${open}${row.replace(BG_CLEARING, (reset) => reset + open)}${BG_RESET}${margin}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Content columns of a panel body (Agents, Todos, Status rail) in the active
|
|
330
|
+
* style. Callers that pre-wrap or pre-fit rows use it so a float panel, two
|
|
331
|
+
* columns narrower than the outlined frame, never re-wraps or clips them.
|
|
332
|
+
*/
|
|
333
|
+
export function panelInnerWidth(theme: CardTheme, width: number, tone: CardTone = CARD_TONE.INFO): number {
|
|
334
|
+
return floatOpener(theme, tone, width) ? cardInnerWidth(Math.floor(width) - FLOAT_MARGIN * 2) : cardInnerWidth(width);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* The row a panel draws its header on in the active style: 0 for the
|
|
339
|
+
* outlined frame, 1 below the float panel's top padding row. Callers
|
|
340
|
+
* hit-test their header control with it.
|
|
341
|
+
*/
|
|
342
|
+
export function panelHeaderRow(theme: CardTheme, width: number, tone: CardTone = CARD_TONE.INFO): number {
|
|
343
|
+
return floatOpener(theme, tone, width) ? 1 : 0;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Rows a float panel with a body adds over the outlined frame: the top
|
|
348
|
+
* padding row and the separator below the header, or 0 for the outlined
|
|
349
|
+
* frame. Callers with a row budget spend them from the body.
|
|
350
|
+
*/
|
|
351
|
+
export function panelExtraRows(theme: CardTheme, width: number, tone: CardTone = CARD_TONE.INFO): number {
|
|
352
|
+
return floatOpener(theme, tone, width) ? 2 : 0;
|
|
353
|
+
}
|
|
354
|
+
|
|
124
355
|
export function renderCard(card: Card, theme: CardTheme, width: number, options: CardRenderOptions): string[] {
|
|
125
|
-
|
|
356
|
+
// Only opt-in tool previews collapse to nothing at nonpositive widths; the
|
|
357
|
+
// legacy path keeps its empty-row shape for widgets that call it unguarded.
|
|
358
|
+
if (options.previewRows !== undefined) return width <= 0 ? [] : floatRows(card.tone, theme, width, (inner) => ({
|
|
359
|
+
head: [cardTop(card, theme, inner, options.hint)],
|
|
360
|
+
body: cardBodyRows(card.body.map((line) => theme.fg(BODY_ROLE, line)), card.tone, theme, inner, options),
|
|
361
|
+
bottom: cardBottom(card.tone, theme, inner),
|
|
362
|
+
}));
|
|
363
|
+
// Without the panel opt-in, legacy cards always keep the outlined frame.
|
|
364
|
+
if (chrome !== OUTLINE_CHROME) return withChrome(OUTLINE_CHROME, () => renderCard(card, theme, width, options));
|
|
365
|
+
const open = options.panel ? floatOpener(theme, card.tone, width) : "";
|
|
366
|
+
if (open) return paintFloat(withChrome(FLOAT_CHROME, () => floatPanel(card, theme, Math.floor(width) - FLOAT_MARGIN * 2, options)), open);
|
|
126
367
|
const top = cardTop(card, theme, width, options.hint);
|
|
127
368
|
const bottom = cardBottom(card.tone, theme, width);
|
|
369
|
+
return [top, ...cardText(card, theme, cardInnerWidth(width), options.expanded).map((line) => cardLine(line, card.tone, theme, width)), bottom];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Body text of a legacy card or panel, wrapped to its content columns, before the side rails. */
|
|
373
|
+
function cardText(card: Card, theme: CardTheme, innerWidth: number, expanded: boolean): string[] {
|
|
128
374
|
const lines = bodyLines(card, innerWidth);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
375
|
+
if (lines.length === 0) return [];
|
|
376
|
+
if (!expanded) {
|
|
377
|
+
const first = lines.find((line) => line !== "") ?? "";
|
|
378
|
+
const clipped = lines.length > 1 ? truncateToWidth(first, Math.max(1, innerWidth - 1), "") + "…" : first;
|
|
379
|
+
return [theme.fg(BODY_ROLE, clipped)];
|
|
380
|
+
}
|
|
381
|
+
return lines.map((line) => (line === "" ? "" : theme.fg(BODY_ROLE, line)));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Float panels: the float card chrome. Padding rows that keep the accent bar
|
|
385
|
+
// sit above the heading, between heading and body (only when a body exists)
|
|
386
|
+
// and in the bottom rule's place, centering the content: panelExtraRows taller
|
|
387
|
+
// than neon, with the heading on row 1 (see panelHeaderRow).
|
|
388
|
+
function floatPanel(card: Card, theme: CardTheme, width: number, options: CardRenderOptions): string[] {
|
|
389
|
+
const blank = cardBottom(card.tone, theme, width);
|
|
390
|
+
const body = cardText(card, theme, cardInnerWidth(width), options.expanded).map((line) => cardLine(line, card.tone, theme, width));
|
|
391
|
+
return [blank, panelHeader(card, theme, width, options.hint), ...(body.length > 0 ? [blank, ...body] : []), blank];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function fitRow(text: string, width: number): string {
|
|
395
|
+
const clipped = visibleWidth(text) <= width ? text : truncateToWidth(text, width, "…");
|
|
396
|
+
return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// `▎ <glyph> <title> <subtitle>` left and the hint right, ending in the same
|
|
400
|
+
// two columns the body rows keep for their right edge.
|
|
401
|
+
function panelHeader(card: Card, theme: CardTheme, width: number, hint?: string): string {
|
|
402
|
+
const lead = theme.fg(FRAME_ROLE[card.tone], chrome.topLeft) + chrome.topLead;
|
|
403
|
+
const subtitle = card.subtitle ? ` ${theme.fg(SUBTITLE_ROLE, card.subtitle)}` : "";
|
|
404
|
+
const left = `${lead}${theme.fg(TITLE_ROLE[card.tone], `${card.glyph ?? CARD_GLYPH} ${card.title}`)}${subtitle}`;
|
|
405
|
+
const right = hint ? `${theme.fg(SUBTITLE_ROLE, hint)}${chrome.topRight}` : "";
|
|
406
|
+
const gap = width - visibleWidth(left) - visibleWidth(right);
|
|
407
|
+
return right && gap >= 1 ? `${left}${" ".repeat(gap)}${right}` : fitRow(left, width);
|
|
139
408
|
}
|
|
@@ -56,7 +56,8 @@ function rule(length: number): string {
|
|
|
56
56
|
|
|
57
57
|
export function colorDiff(text: string, theme: ChangesViewTheme): string[] {
|
|
58
58
|
const lines: string[] = [];
|
|
59
|
-
for (const
|
|
59
|
+
for (const rawLine of text.split("\n")) {
|
|
60
|
+
const line = sanitizeTerminalText(rawLine.replace(/\r/g, "")).replace(/\t/g, " ");
|
|
60
61
|
if (line === "" || HEADER_PREFIXES.some((prefix) => line.startsWith(prefix))) continue;
|
|
61
62
|
if (line.startsWith("@@")) lines.push(theme.fg(ROLE.HUNK, line));
|
|
62
63
|
else if (line.startsWith("+")) lines.push(theme.fg("toolDiffAdded", line));
|
package/lib/shell-prompt.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
2
|
import { stripAnsi } from "./terminal-theme.ts";
|
|
3
|
+
import { CARD_STYLE, type CardStyle } from "./shell-card.ts";
|
|
3
4
|
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// with a petal that shows what the agent is doing. Everything here is pure.
|
|
5
|
+
// Pure prompt chrome: neon keeps Pi's rule/content/rule rows in a rounded
|
|
6
|
+
// frame; float retains those indices as painted status/content/padding.
|
|
7
7
|
|
|
8
8
|
export const PROMPT_STATE = {
|
|
9
9
|
IDLE: "idle",
|
|
@@ -43,6 +43,8 @@ export function scanWorkingText(text: string, tick: number, fg: (role: string, t
|
|
|
43
43
|
const PETAL_TONE_FRAMES = [PETAL_TONE.DEEP, PETAL_TONE.SOFT, PETAL_TONE.ROSE, PETAL_TONE.BRIGHT, PETAL_TONE.ROSE, PETAL_TONE.SOFT, PETAL_TONE.DEEP, PETAL_TONE.DEEP] as const;
|
|
44
44
|
|
|
45
45
|
export interface PromptFrameOptions {
|
|
46
|
+
style?: CardStyle;
|
|
47
|
+
bg?: (role: string, text: string) => string;
|
|
46
48
|
state: PromptState;
|
|
47
49
|
tick: number;
|
|
48
50
|
borderColor: (text: string) => string;
|
|
@@ -50,11 +52,17 @@ export interface PromptFrameOptions {
|
|
|
50
52
|
bold?: (text: string) => string;
|
|
51
53
|
/**
|
|
52
54
|
* Overrides the editor's own bottom scroll indicator (e.g. "esc again to
|
|
53
|
-
* cancel"). Both share the bottom rule
|
|
55
|
+
* cancel"). Both share the bottom rule / float hint slot; an explicit
|
|
54
56
|
* hint always wins because it reflects state the editor cannot render on
|
|
55
57
|
* its own.
|
|
56
58
|
*/
|
|
57
59
|
escHint?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Overrides the generic "working…" label while state is WORKING with an
|
|
62
|
+
* explicit, orchestrator-reported ODD phase label (e.g. "exploring…").
|
|
63
|
+
* Ignored outside the WORKING state; undefined falls back to "working…".
|
|
64
|
+
*/
|
|
65
|
+
workingLabel?: string;
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
// A terminal cell cannot grow, so the petal earns presence with weight and
|
|
@@ -93,8 +101,13 @@ function rule(length: number): string {
|
|
|
93
101
|
return "─".repeat(Math.max(0, length));
|
|
94
102
|
}
|
|
95
103
|
|
|
104
|
+
function stateLabel(options: PromptFrameOptions): string | undefined {
|
|
105
|
+
if (options.state === PROMPT_STATE.WORKING) return options.workingLabel ?? STATE_LABEL[PROMPT_STATE.WORKING];
|
|
106
|
+
return STATE_LABEL[options.state];
|
|
107
|
+
}
|
|
108
|
+
|
|
96
109
|
function topRule(width: number, options: PromptFrameOptions, indicator: string | undefined): string {
|
|
97
|
-
const label = indicator ??
|
|
110
|
+
const label = indicator ?? stateLabel(options);
|
|
98
111
|
const scanning = options.state === PROMPT_STATE.WORKING;
|
|
99
112
|
const glyph = petalGlyph(options.state, options.tick);
|
|
100
113
|
const petal = options.fg(petalTone(options.state, options.tick), options.bold ? options.bold(glyph) : glyph);
|
|
@@ -120,8 +133,91 @@ function sideRules(line: string, innerWidth: number, options: PromptFrameOptions
|
|
|
120
133
|
return options.borderColor("│") + content + options.borderColor("│");
|
|
121
134
|
}
|
|
122
135
|
|
|
123
|
-
export
|
|
136
|
+
export interface PromptLayout {
|
|
137
|
+
width: number;
|
|
138
|
+
nativeWidth: number;
|
|
139
|
+
prefixWidth: number;
|
|
140
|
+
background: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Resolve fallback before native wrapping, selection, completion and mouse geometry. */
|
|
144
|
+
export function resolvePromptLayout(width: number, style?: CardStyle, bg?: PromptFrameOptions["bg"]): PromptLayout {
|
|
145
|
+
width = Math.max(0, Math.floor(width));
|
|
146
|
+
let background = "";
|
|
147
|
+
if (style === CARD_STYLE.FLOAT && width >= 10 && bg) {
|
|
148
|
+
try {
|
|
149
|
+
const painted = bg("toolSuccessBg", " ");
|
|
150
|
+
const match = painted.match(/^((?:\x1b\[[\d;]*m)+) (?:\x1b\[(?:49|0)?m)$/);
|
|
151
|
+
if (match) {
|
|
152
|
+
for (const sgr of match[1].matchAll(/\x1b\[([\d;]*)m/g)) {
|
|
153
|
+
for (const command of promptSgrCommands(sgr[1])) {
|
|
154
|
+
const code = command[0];
|
|
155
|
+
if (code === 0 || code === 49) background = "";
|
|
156
|
+
else if ((code >= 40 && code <= 47) || (code >= 100 && code <= 107) ||
|
|
157
|
+
(code === 48 && ((command[1] === 5 && command.length === 3) || (command[1] === 2 && command.length === 5)))) {
|
|
158
|
+
background = `\x1b[${command.join(";")}m`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch { /* Missing theme background: retain the entire neon path. */ }
|
|
164
|
+
}
|
|
165
|
+
return { width, nativeWidth: Math.max(1, width - (background ? 5 : 2)), prefixWidth: background ? 3 : 1, background };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Extended-colour parameters belong to one command: zero RGB channels
|
|
169
|
+
// must never be interpreted as full styling resets.
|
|
170
|
+
function promptSgrCommands(parameters: string): number[][] {
|
|
171
|
+
const codes = parameters.split(";").map(Number);
|
|
172
|
+
const commands: number[][] = [];
|
|
173
|
+
for (let i = 0; i < codes.length;) {
|
|
174
|
+
const extended = codes[i] === 38 || codes[i] === 48 || codes[i] === 58;
|
|
175
|
+
const length = extended && codes[i + 1] === 2 ? 5 : extended && codes[i + 1] === 5 ? 3 : 1;
|
|
176
|
+
commands.push(codes.slice(i, i + length));
|
|
177
|
+
i += length;
|
|
178
|
+
}
|
|
179
|
+
return commands;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Rearm only the panel background, leaving marker/inversion bytes intact.
|
|
183
|
+
function rearmPromptBackground(text: string, background: string): string {
|
|
184
|
+
return text.replace(/\x1b\[([\d;]*)m/g, (sgr, parameters: string) =>
|
|
185
|
+
promptSgrCommands(parameters).some(([code]) => code === 0 || code === 49) ? sgr + background : sgr);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Same chrome and horizontal prefix for editable and native completion rows. */
|
|
189
|
+
export function floatPromptRow(line: string, layout: PromptLayout, borderColor: PromptFrameOptions["borderColor"]): string {
|
|
190
|
+
const clipped = truncateToWidth(line, layout.nativeWidth, "");
|
|
191
|
+
const content = borderColor("▎") + " " + clipped + " ".repeat(Math.max(0, layout.nativeWidth - visibleWidth(clipped))) + " ";
|
|
192
|
+
return " " + layout.background + rearmPromptBackground(content, layout.background) + "\x1b[49m ";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function floatPromptHint(top: string, bottom: string, options: PromptFrameOptions, width: number): string {
|
|
196
|
+
const glyph = petalGlyph(options.state, options.tick);
|
|
197
|
+
const petal = options.fg(petalTone(options.state, options.tick), options.bold ? options.bold(glyph) : glyph);
|
|
198
|
+
const label = stateLabel(options) ?? "waiting for input";
|
|
199
|
+
let paintedLabel = "";
|
|
200
|
+
if (label) {
|
|
201
|
+
paintedLabel = options.state === PROMPT_STATE.WORKING
|
|
202
|
+
? scanWorkingText(label, options.tick, options.fg)
|
|
203
|
+
: options.fg(LABEL_ROLE, label);
|
|
204
|
+
}
|
|
205
|
+
const info = [scrollIndicator(top), options.escHint ?? scrollIndicator(bottom)]
|
|
206
|
+
.filter((text): text is string => Boolean(text))
|
|
207
|
+
.map((text) => options.fg(LABEL_ROLE, text));
|
|
208
|
+
const hint = truncateToWidth(" " + petal + (paintedLabel ? " " + paintedLabel : "") + (info.length ? " · " + info.join(" · ") : ""), width, "");
|
|
209
|
+
return hint + " ".repeat(Math.max(0, width - visibleWidth(hint)));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function framePromptLines(lines: string[], width: number, options: PromptFrameOptions, layout = resolvePromptLayout(width, options.style, options.bg)): string[] {
|
|
124
213
|
width = Math.max(0, Math.floor(width));
|
|
214
|
+
if (layout.background && lines.length >= 2) {
|
|
215
|
+
return [
|
|
216
|
+
floatPromptRow(floatPromptHint(lines[0], lines[lines.length - 1], options, layout.nativeWidth).slice(2), layout, options.borderColor),
|
|
217
|
+
...lines.slice(1, -1).map((line) => floatPromptRow(line, layout, options.borderColor)),
|
|
218
|
+
floatPromptRow("", layout, options.borderColor),
|
|
219
|
+
];
|
|
220
|
+
}
|
|
125
221
|
if (lines.length < 2) return lines.map((line) => truncateToWidth(line, width, ""));
|
|
126
222
|
if (width < 2) return lines.map((_line, index) => width === 0 ? "" : options.borderColor(index === 0 ? "╭" : index === lines.length - 1 ? "╰" : "│"));
|
|
127
223
|
const innerWidth = width - 2;
|