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
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { realpathSync } from "node:fs";
|
|
3
2
|
import test from "node:test";
|
|
4
3
|
import { initTheme, keyHint } from "@earendil-works/pi-coding-agent";
|
|
5
4
|
import { imageFallback, visibleWidth } from "@earendil-works/pi-tui";
|
|
6
5
|
import { cardBody, cardHint, cardTitle, cardTone } from "./gentle-card-text.ts";
|
|
6
|
+
import { CARD_STYLE, cardStyle, setCardStyle } from "../lib/shell-card.ts";
|
|
7
|
+
import { stripAnsi } from "../lib/terminal-theme.ts";
|
|
7
8
|
import piPretty from "../extensions/pi-pretty.ts";
|
|
8
|
-
import
|
|
9
|
+
import productionQuietTools, {
|
|
10
|
+
createQuietToolRenderer,
|
|
9
11
|
countNonEmptyLines,
|
|
10
12
|
extractTextContent,
|
|
11
13
|
formatToolResultOutput,
|
|
@@ -50,10 +52,37 @@ function routineRenderContext(overrides: Record<string, unknown> = {}) {
|
|
|
50
52
|
};
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
|
|
55
|
+
// Content assertions read the quiet tool card content: the top rule becomes its
|
|
56
|
+
// call text (glyph, fill, and hint dropped), sides are stripped, and the closing
|
|
57
|
+
// rule is dropped. Gentle AI cards keep their raw frame for the card-text
|
|
58
|
+
// helpers, and the quiet card tests assert the raw frame via frameLines.
|
|
59
|
+
// Legacy bash argument assertions drop its new identity prefix here; distinct
|
|
60
|
+
// tool identity is asserted separately against the actual unmodified frame.
|
|
61
|
+
// A pending call's `running…` row is frame chrome too, asserted separately.
|
|
62
|
+
const TAG = "(?:</?[a-zA-Z]+>)*";
|
|
63
|
+
const CARD_TOP = new RegExp(`^${TAG}╭─ ${TAG}[✿≡$⌕⌖☷✎+]${TAG} (.*?) ${TAG}─+${TAG}(?: .*? )?${TAG}╮${TAG}$`);
|
|
64
|
+
const CARD_SIDE = new RegExp(`^${TAG}│${TAG} (.*) ${TAG}│${TAG}$`);
|
|
65
|
+
const CARD_BOTTOM = new RegExp(`^${TAG}╰${TAG}─*${TAG}╯${TAG}$`);
|
|
66
|
+
|
|
67
|
+
function frameLines(component: { render(width: number): string[] }, width = 120): string[] {
|
|
54
68
|
return component.render(width).map((line) => line.trimEnd());
|
|
55
69
|
}
|
|
56
70
|
|
|
71
|
+
const QUIET_CARD_COMPONENTS = new Set(["ToolCardTop", "ToolCardBody"]);
|
|
72
|
+
|
|
73
|
+
function renderLines(component: { render(width: number): string[] }, width = 120): string[] {
|
|
74
|
+
if (!QUIET_CARD_COMPONENTS.has(component.constructor.name)) return component.render(width).map((line) => line.trimEnd());
|
|
75
|
+
const call = component.constructor.name === "ToolCardTop";
|
|
76
|
+
return component.render(width).flatMap((line) => {
|
|
77
|
+
if (CARD_BOTTOM.test(line)) return [];
|
|
78
|
+
const top = CARD_TOP.exec(line);
|
|
79
|
+
if (top) return [top[1]!.replace(/^(?:<[^>]+>)*bash(?:<[^>]+>)* /, "").trimEnd()];
|
|
80
|
+
const side = CARD_SIDE.exec(line);
|
|
81
|
+
const text = (side ? side[1]! : line).trimEnd();
|
|
82
|
+
return call && side && text.replace(/<\/?[a-zA-Z]+>/g, "") === "running…" ? [] : [text];
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
57
86
|
function renderToString(component: { render(width: number): string[] }, width = 120): string {
|
|
58
87
|
return renderLines(component, width).join("\n");
|
|
59
88
|
}
|
|
@@ -141,6 +170,15 @@ async function withEnvAsync<T>(updates: Record<string, string | undefined>, run:
|
|
|
141
170
|
}
|
|
142
171
|
}
|
|
143
172
|
|
|
173
|
+
// Standalone rendering-only fixture for historical Bash formatting contracts.
|
|
174
|
+
// No executor, shellPath, or claim of production native-Bash UI parity.
|
|
175
|
+
function quietTools(pi: any, resolveOverride?: any) {
|
|
176
|
+
productionQuietTools(pi, resolveOverride);
|
|
177
|
+
if (process.env.GENTLE_PI_QUIET_TOOLS !== "0") {
|
|
178
|
+
pi.registerTool({ name: "bash", ...createQuietToolRenderer("bash", resolveOverride) });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
144
182
|
function registeredQuietTools() {
|
|
145
183
|
const { pi, tools } = createPi();
|
|
146
184
|
withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
|
|
@@ -161,17 +199,18 @@ function assertGenericBash(tool: any, command: string): void {
|
|
|
161
199
|
const call = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
|
|
162
200
|
const output = renderToolResult(tool, textResult("original command output"), { expanded: true, isPartial: false }, { args: { command } });
|
|
163
201
|
assert.equal(call.trimEnd(), `$ ${command}`, command);
|
|
164
|
-
assert.doesNotMatch(call,
|
|
202
|
+
assert.doesNotMatch(call, /🌹/);
|
|
165
203
|
assert.match(output, /original command output/);
|
|
166
204
|
}
|
|
167
205
|
|
|
168
|
-
test("quiet
|
|
206
|
+
test("quiet tools register six overrides and codemode, never Bash", () => {
|
|
169
207
|
withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => {
|
|
170
208
|
const { pi, tools } = createPi();
|
|
171
209
|
|
|
172
|
-
|
|
210
|
+
productionQuietTools(pi as any);
|
|
173
211
|
|
|
174
|
-
|
|
212
|
+
assert.equal(tools.has("bash"), false);
|
|
213
|
+
for (const toolName of ["read", "grep", "find", "ls", "edit", "write", "codemode"]) {
|
|
175
214
|
const tool = tools.get(toolName);
|
|
176
215
|
assert.ok(tool, `missing quiet renderer for ${toolName}`);
|
|
177
216
|
assert.equal(tool.renderShell, "self", `${toolName} must opt out of Pi's painted Box`);
|
|
@@ -181,24 +220,19 @@ test("quiet tool rendering registers noisy built-in tools", () => {
|
|
|
181
220
|
});
|
|
182
221
|
});
|
|
183
222
|
|
|
184
|
-
test("quiet
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
};
|
|
193
|
-
const output = extractTextContent(
|
|
194
|
-
await tool.execute("tool-call", { command: "pwd" }, new AbortController().signal, undefined, context),
|
|
195
|
-
).trim();
|
|
196
|
-
// pwd prints the physical directory: on macOS /tmp is a symlink to /private/tmp.
|
|
197
|
-
assert.equal(output, realpathSync("/tmp"));
|
|
198
|
-
assert.notEqual(output, process.cwd());
|
|
223
|
+
test("quiet tools never replace an existing native Bash tool", () => {
|
|
224
|
+
withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => {
|
|
225
|
+
const { pi, tools } = createPi({ throwOnToolConflict: true });
|
|
226
|
+
const native = createSdkTool("bash");
|
|
227
|
+
pi.registerTool(native);
|
|
228
|
+
productionQuietTools(pi as any);
|
|
229
|
+
assert.strictEqual(tools.get("bash"), native);
|
|
230
|
+
});
|
|
199
231
|
});
|
|
200
232
|
|
|
201
|
-
|
|
233
|
+
// Actual configured Bash execution is covered in quiet-bash-runtime.test.ts.
|
|
234
|
+
|
|
235
|
+
test("standalone Bash rendering fixture does not interpret shellPath", () => {
|
|
202
236
|
const tool = registeredQuietTools().get("bash");
|
|
203
237
|
const render = (context: Record<string, unknown> = {}) => renderToString(tool.renderCall({ command: "printf output" }, passthroughTheme, routineRenderContext(context)));
|
|
204
238
|
const configured = render({ shellPath: "/configured/bash" });
|
|
@@ -210,7 +244,7 @@ test("quiet tool rendering can be disabled by env", () => {
|
|
|
210
244
|
withEnv({ GENTLE_PI_QUIET_TOOLS: "0" }, () => {
|
|
211
245
|
const { pi, tools } = createPi();
|
|
212
246
|
|
|
213
|
-
|
|
247
|
+
productionQuietTools(pi as any);
|
|
214
248
|
|
|
215
249
|
assert.equal(tools.size, 0);
|
|
216
250
|
});
|
|
@@ -223,9 +257,10 @@ test("pi-pretty suppresses overlapping tools before quiet tools register", async
|
|
|
223
257
|
const { pi, tools } = createPi({ throwOnToolConflict: true });
|
|
224
258
|
|
|
225
259
|
await piPretty(pi as any, fakePiPrettyDeps as any);
|
|
226
|
-
|
|
260
|
+
productionQuietTools(pi as any);
|
|
227
261
|
|
|
228
|
-
|
|
262
|
+
assert.equal(tools.has("bash"), false);
|
|
263
|
+
for (const toolName of ["read", "grep", "find", "ls", "edit", "write"]) {
|
|
229
264
|
assert.ok(tools.has(toolName), `missing quiet tool ${toolName}`);
|
|
230
265
|
}
|
|
231
266
|
assert.equal(process.env.PRETTY_DISABLE_TOOLS, "multi_grep,read,bash,ls,find,grep");
|
|
@@ -288,9 +323,10 @@ test("quiet tool rendering uses bounded previews while preserving search summari
|
|
|
288
323
|
);
|
|
289
324
|
assert.match(read, /first line\nsecond line/);
|
|
290
325
|
assert.match(bash, /stdout line\nstderr line/);
|
|
326
|
+
// The expand key rides the call card's top rule, never the result body.
|
|
291
327
|
const configuredHint = keyHint("app.tools.expand", "to expand");
|
|
292
|
-
assert.ok(read.includes(configuredHint));
|
|
293
|
-
assert.ok(bash.includes(configuredHint));
|
|
328
|
+
assert.ok(!read.includes(configuredHint));
|
|
329
|
+
assert.ok(!bash.includes(configuredHint));
|
|
294
330
|
|
|
295
331
|
for (const [tool, text, label] of [
|
|
296
332
|
["grep", "src/a.ts:1:match\nsrc/b.ts:2:match", "2 matches"],
|
|
@@ -304,7 +340,7 @@ test("quiet tool rendering uses bounded previews while preserving search summari
|
|
|
304
340
|
tools.get(tool).renderResult(textResult(text), { expanded: true, isPartial: false }, passthroughTheme, {}),
|
|
305
341
|
);
|
|
306
342
|
assert.match(collapsed, new RegExp(label));
|
|
307
|
-
assert.
|
|
343
|
+
assert.match(collapsed, /src\/a\.ts|file-a\.ts/);
|
|
308
344
|
assert.match(expanded, new RegExp(text.split("\n")[1]!));
|
|
309
345
|
}
|
|
310
346
|
});
|
|
@@ -343,18 +379,18 @@ test("quiet search and placeholder summaries preserve exact-result behavior", as
|
|
|
343
379
|
const tools = registeredQuietTools();
|
|
344
380
|
const hint = keyHint("app.tools.expand", "to expand");
|
|
345
381
|
const cases = [
|
|
346
|
-
["grep", "context rows count only match rows", "grep", contextText, { context: 1 }, " → 5 matches", undefined, /5 matches/, /15 matches/, 1],
|
|
382
|
+
["grep", "context rows count only match rows", "grep", contextText, { context: 1 }, " → 5 matches", undefined, /5 matches[\s\S]*first match/, /15 matches/, 1],
|
|
347
383
|
["grep", "no-context output counts every non-empty row", "grep", "src/a.ts:1:match\nsrc/b.ts:2:match", {}, " → 2 matches", undefined, /2 matches/, undefined, 1],
|
|
348
384
|
["exact", "current empty-directory marker", "ls", "(empty directory)", {}, "", undefined, /^$/, /entries|to expand/, 0],
|
|
349
385
|
["exact", "legacy empty-directory marker", "ls", "Directory is empty", {}, "", undefined, /^$/, /entries|to expand/, 0],
|
|
350
386
|
["exact", "no-output marker", "bash", "(no output)", { command: "true" }, "\n(no output)", undefined, /^\(no output\)$/, /to expand/, 0],
|
|
351
|
-
["collision", "empty-directory marker with a real entry", "ls", lsCollisionText, {}, " → 2 entries", `\n${lsCollisionText}`, /2 entries
|
|
387
|
+
["collision", "empty-directory marker with a real entry", "ls", lsCollisionText, {}, " → 2 entries", `\n${lsCollisionText}`, /2 entries[\s\S]*real-entry\.txt/, undefined, 1],
|
|
352
388
|
["collision", "no-output marker with real output", "bash", bashCollisionText, { command: "printf output" }, `\n${bashCollisionText}`, `\n${bashCollisionText}`, /\(no output\)\nreal output/, undefined, 1],
|
|
353
389
|
] as const;
|
|
354
390
|
assert.equal(cases.length, 7);
|
|
355
391
|
|
|
356
392
|
const assertSummaryCase = (current: (typeof cases)[number]) => {
|
|
357
|
-
const [, name, toolName, text, args, collapsed, expanded, rendered, forbidden
|
|
393
|
+
const [, name, toolName, text, args, collapsed, expanded, rendered, forbidden] = current;
|
|
358
394
|
const result = textResult(text) as any;
|
|
359
395
|
const tool = tools.get(toolName);
|
|
360
396
|
assert.ok(tool, `${name}: missing ${toolName} renderer`);
|
|
@@ -363,7 +399,7 @@ test("quiet search and placeholder summaries preserve exact-result behavior", as
|
|
|
363
399
|
const output = renderToolResult(tool, result, { expanded: false, isPartial: false }, { args });
|
|
364
400
|
assert.match(output, rendered, name);
|
|
365
401
|
if (forbidden) assert.doesNotMatch(output, forbidden, name);
|
|
366
|
-
assert.equal(output.split(hint).length - 1,
|
|
402
|
+
assert.equal(output.split(hint).length - 1, 0, `${name}: the expand key rides the call card, not the result`);
|
|
367
403
|
};
|
|
368
404
|
|
|
369
405
|
for (const [section, count] of [["grep", 2], ["exact", 3], ["collision", 2]] as const) await t.test(section, () => {
|
|
@@ -408,9 +444,9 @@ test("quiet Bash errors preserve meaningful rows for both public output ordering
|
|
|
408
444
|
assert.ok(collapsedLines.length <= 4, `${name}: expected at most 4 visual rows, got ${collapsedLines.length}`);
|
|
409
445
|
assert.deepEqual(collapsedLines.slice(0, 3), previewRows, `${name}: preview rows`);
|
|
410
446
|
assert.equal(collapsedLines.filter((line) => line.trim().length === 0).length, 0, `${name}: no blank preview rows`);
|
|
411
|
-
assert.equal(collapsedLines.filter((line) => line.includes(hint)).length,
|
|
447
|
+
assert.equal(collapsedLines.filter((line) => line.includes(hint)).length, 0, `${name}: the expand key rides the call card`);
|
|
412
448
|
const expanded = renderToString(renderResult(result, true), 1000);
|
|
413
|
-
assert.equal(expanded,
|
|
449
|
+
assert.equal(expanded, renderedExpandedText, `${name}: complete expanded sanitized text`);
|
|
414
450
|
assert.doesNotMatch(expanded, /\x1b\[/, `${name}: expanded output is ANSI-free`);
|
|
415
451
|
}
|
|
416
452
|
});
|
|
@@ -439,20 +475,38 @@ test("quiet Bash normalizes compact JSON for bounded previews while keeping expa
|
|
|
439
475
|
assert.equal(previewRows.length, 3);
|
|
440
476
|
assert.ok(collapsedLines.length <= 4, `expected at most 4 visual rows, got ${collapsedLines.length}`);
|
|
441
477
|
assert.deepEqual(collapsedLines.slice(0, 3), previewRows);
|
|
442
|
-
assert.equal(collapsedLines.filter((line) => line.includes(keyHint("app.tools.expand", "to expand"))).length,
|
|
478
|
+
assert.equal(collapsedLines.filter((line) => line.includes(keyHint("app.tools.expand", "to expand"))).length, 0);
|
|
443
479
|
assert.doesNotMatch(collapsed, /longTail|this must stay out of the collapsed preview|deeper/);
|
|
444
480
|
|
|
445
481
|
const expanded = renderToString(tool.renderResult(result, { expanded: true, isPartial: false }, passthroughTheme, { args: commandArgs }), 1000);
|
|
446
|
-
assert.equal(expanded,
|
|
482
|
+
assert.equal(expanded, compactJson);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
test("retired SDD commands use generic bash rendering while review remains private", () => {
|
|
486
|
+
const { pi, tools } = createPi();
|
|
487
|
+
withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
|
|
488
|
+
const bash = tools.get("bash");
|
|
489
|
+
for (const command of [
|
|
490
|
+
"gentle-ai sdd-status rose --json",
|
|
491
|
+
"env FOO=bar gentle-ai sdd-continue rose",
|
|
492
|
+
"gentle-ai sdd-attempt grant --authorization-root /root",
|
|
493
|
+
"./.gentle-ai/v2.2.0/gentle-ai sdd-status rose --json",
|
|
494
|
+
"C:\\package\\.gentle-ai\\v2.2.0\\gentle-ai.exe sdd-continue rose",
|
|
495
|
+
]) {
|
|
496
|
+
assert.equal(gentleAiRoutineCommand({ command }), undefined, command);
|
|
497
|
+
const call = renderToString(bash.renderCall({ command }, passthroughTheme, { args: { command } }));
|
|
498
|
+
assert.equal(call, `$ ${command}`);
|
|
499
|
+
const result = renderToString(bash.renderResult(textResult("ordinary output"), { expanded: false, isPartial: false }, passthroughTheme, { args: { command } }));
|
|
500
|
+
assert.match(result, /ordinary output/);
|
|
501
|
+
}
|
|
502
|
+
const review = "gentle-ai review status --next-transition";
|
|
503
|
+
assert.equal(gentleAiRoutineCommand({ command: review }), "review");
|
|
504
|
+
assert.equal(cardTitle(renderToString(bash.renderCall({ command: review }, passthroughTheme, { args: { command: review } }))), "🌹 rdd running · status");
|
|
447
505
|
});
|
|
448
506
|
|
|
449
|
-
test("quiet tool rendering identifies only routine Gentle AI
|
|
450
|
-
assert.equal(gentleAiRoutineCommand({ command: "gentle-ai sdd-status fix-rose --json" }), "sdd-status");
|
|
451
|
-
assert.equal(gentleAiRoutineCommand({ command: "env FOO=bar gentle-ai sdd-continue fix-rose" }), "sdd-continue");
|
|
507
|
+
test("quiet tool rendering identifies only routine Gentle AI review commands", () => {
|
|
452
508
|
assert.equal(gentleAiRoutineCommand({ command: "/package/.gentle-ai/v2.2.0/gentle-ai review status --next-transition" }), "review");
|
|
453
|
-
assert.equal(gentleAiRoutineCommand({ command: "./.gentle-ai/v2.2.0/gentle-ai sdd-status rose --json" }), "sdd-status");
|
|
454
509
|
assert.equal(gentleAiRoutineCommand({ command: ".\\.gentle-ai\\v2.2.0\\gentle-ai.exe review status --next-transition" }), "review");
|
|
455
|
-
assert.equal(gentleAiRoutineCommand({ command: "C:\\package\\.gentle-ai\\v2.2.0\\gentle-ai.exe sdd-continue rose" }), "sdd-continue");
|
|
456
510
|
assert.equal(gentleAiRoutineCommand({ command: "gentle-ai sdd-attempt acquire --change fix-rose" }), undefined);
|
|
457
511
|
assert.equal(gentleAiRoutineCommand({ command: "gentle-ai sdd-attempt settle --change fix-rose" }), undefined);
|
|
458
512
|
assert.equal(gentleAiRoutineCommand({ command: "gentle-ai review status --next-transition" }), "review");
|
|
@@ -492,11 +546,11 @@ test("quiet tool rendering refuses to compact composed Gentle AI shell commands"
|
|
|
492
546
|
}
|
|
493
547
|
});
|
|
494
548
|
|
|
495
|
-
test("quiet tool rendering
|
|
549
|
+
test("quiet tool rendering previews direct Gentle AI results and preserves expanded errors", () => {
|
|
496
550
|
const { pi, tools } = createPi();
|
|
497
551
|
withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
|
|
498
552
|
const command = "gentle-ai review status --next-transition";
|
|
499
|
-
const
|
|
553
|
+
const rose = "\u{1F339}";
|
|
500
554
|
const tool = tools.get("bash");
|
|
501
555
|
const failureText = "review status failed: authority unavailable\x1b[31m\nlineage=secret";
|
|
502
556
|
const expandHint = keyHint("app.tools.expand", "to expand");
|
|
@@ -509,16 +563,15 @@ test("quiet tool rendering hides every collapsed direct Gentle AI result and pre
|
|
|
509
563
|
const empty = renderToString(tool.renderResult(textResult(""), { expanded: false, isPartial: false }, passthroughTheme, { args: { command } }));
|
|
510
564
|
const nonText = renderToString(tool.renderResult({ content: [{ type: "image", data: "opaque", mimeType: "image/png" }] }, { expanded: false, isPartial: false }, passthroughTheme, { args: { command } }));
|
|
511
565
|
|
|
512
|
-
assert.equal([...
|
|
513
|
-
assert.equal(
|
|
514
|
-
assert.
|
|
515
|
-
assert.
|
|
516
|
-
assert.doesNotMatch(collapsed,
|
|
517
|
-
assert.equal(cardBody(expanded)
|
|
518
|
-
assert.match(expanded, /"next_transition":"stop"/);
|
|
566
|
+
assert.equal([...rose].length, 1, "the rose is a plain emoji, no presentation selector");
|
|
567
|
+
assert.equal(visibleWidth(rose), 2);
|
|
568
|
+
assert.equal(cardTitle(call), `🌹 rdd running · status`);
|
|
569
|
+
assert.equal(cardBody(collapsed), "stop", "a JSON envelope collapses to its readable summary");
|
|
570
|
+
assert.doesNotMatch(collapsed, /\{"|next_transition/);
|
|
571
|
+
assert.equal(cardBody(expanded), '{\n "next_transition": "stop"\n}', "expanded JSON is pretty-printed");
|
|
519
572
|
assert.doesNotMatch(expanded, /to expand/);
|
|
520
|
-
assert.match(cardBody(failure),
|
|
521
|
-
assert.
|
|
573
|
+
assert.match(cardBody(failure), /review status failed.*authority unavailable/);
|
|
574
|
+
assert.match(failure, /lineage=secret/);
|
|
522
575
|
assert.match(expandedFailure, /review status failed: authority unavailable/);
|
|
523
576
|
assert.match(expandedFailure, /lineage=secret/);
|
|
524
577
|
assert.doesNotMatch(expandedFailure, /to expand/);
|
|
@@ -566,16 +619,16 @@ test("quiet tool rendering transitions one Gentle AI header through lifecycle st
|
|
|
566
619
|
assert.strictEqual(initial, running);
|
|
567
620
|
assert.strictEqual(running, completed);
|
|
568
621
|
assert.strictEqual(completed, failed);
|
|
569
|
-
assert.equal(cardTitle(initialText), "
|
|
570
|
-
assert.equal(cardTitle(runningText), "
|
|
571
|
-
assert.equal(cardTitle(completedText), "
|
|
572
|
-
assert.equal(cardTitle(failedText), "
|
|
622
|
+
assert.equal(cardTitle(initialText), "🌹 rdd running · status"); assert.equal(cardTone(initialText), "warning");
|
|
623
|
+
assert.equal(cardTitle(runningText), "🌹 rdd running · status"); assert.equal(cardTone(runningText), "warning");
|
|
624
|
+
assert.equal(cardTitle(completedText), "🌹 rdd status"); assert.equal(cardTone(completedText), "success");
|
|
625
|
+
assert.equal(cardTitle(failedText), "🌹 rdd failed · status"); assert.equal(cardTone(failedText), "error");
|
|
573
626
|
assert.doesNotMatch(failedText, /private-change/);
|
|
574
627
|
});
|
|
575
628
|
|
|
576
629
|
test("quiet Bash keeps direct Gentle AI calls seamless while streaming", () => {
|
|
577
630
|
const tool = registeredQuietTools().get("bash");
|
|
578
|
-
const rose =
|
|
631
|
+
const rose = /🌹/;
|
|
579
632
|
const render = (command: string, state: Record<string, unknown>, overrides: Record<string, unknown> = {}) => {
|
|
580
633
|
const component = tool.renderCall({ command }, passthroughTheme, routineRenderContext({ args: { command }, state, argsComplete: false, ...overrides }));
|
|
581
634
|
return [component, renderToString(component)] as const;
|
|
@@ -583,14 +636,14 @@ test("quiet Bash keeps direct Gentle AI calls seamless while streaming", () => {
|
|
|
583
636
|
const command = "gentle-ai review status --token 'lineage-secret";
|
|
584
637
|
const state = {};
|
|
585
638
|
const [preparing, collapsed] = render(command, state);
|
|
586
|
-
assert.equal(cardTitle(collapsed), "
|
|
639
|
+
assert.equal(cardTitle(collapsed), "🌹 rdd preparing · status");
|
|
587
640
|
assert.doesNotMatch(collapsed, /token|lineage-secret/);
|
|
588
641
|
const expanded = render(command, state, { expanded: true, lastComponent: preparing })[1];
|
|
589
|
-
assert.equal(cardTitle(expanded), "
|
|
642
|
+
assert.equal(cardTitle(expanded), "🌹 rdd preparing · status"); assert.equal(cardBody(expanded), "$ gentle-ai review status --token 'lineage-secret");
|
|
590
643
|
|
|
591
644
|
const direct = "gentle-ai review status";
|
|
592
645
|
const [running, runningText] = render(direct, state, { argsComplete: true, executionStarted: true, lastComponent: preparing });
|
|
593
|
-
assert.strictEqual(running, preparing); assert.equal(cardTitle(runningText), "
|
|
646
|
+
assert.strictEqual(running, preparing); assert.equal(cardTitle(runningText), "🌹 rdd running · status");
|
|
594
647
|
const genericState = {};
|
|
595
648
|
const [generic, genericText] = render("gentle-ai review status | cat", genericState);
|
|
596
649
|
assert.doesNotMatch(genericText, rose); assert.match(genericText, /\| cat/);
|
|
@@ -607,19 +660,14 @@ test("quiet Bash keeps direct Gentle AI calls seamless while streaming", () => {
|
|
|
607
660
|
assert.equal(genericResult.split("\n")[0], "generic result");
|
|
608
661
|
const directState = {}; render(direct, directState, { argsComplete: true });
|
|
609
662
|
const directResult = renderToolResult(tool, textResult("direct result"), { expanded: false, isPartial: false }, { args: { command: direct }, state: directState, argsComplete: true });
|
|
610
|
-
assert.match(cardBody(directResult),
|
|
663
|
+
assert.match(cardBody(directResult), /direct result/);
|
|
611
664
|
});
|
|
612
665
|
|
|
613
666
|
test("quiet tool rendering displays only finite safe Gentle AI operation paths", () => {
|
|
614
667
|
const { pi, tools } = createPi();
|
|
615
668
|
withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
|
|
616
669
|
const tool = tools.get("bash");
|
|
617
|
-
const rose = "🌹︎";
|
|
618
670
|
const cases = [
|
|
619
|
-
["gentle-ai sdd-status change-123 --cwd /repo/private", "sdd status"],
|
|
620
|
-
["gentle-ai sdd-continue change-123 --json", "sdd continue"],
|
|
621
|
-
["gentle-ai sdd-attempt acquire --change change-123", "sdd attempt"],
|
|
622
|
-
["gentle-ai sdd-attempt settle --change change-123 --actor maintainer", "sdd attempt"],
|
|
623
671
|
["gentle-ai review capabilities --cwd /repo/private", "review capabilities"],
|
|
624
672
|
["gentle-ai review start --target sha256:secret --path src/private.ts", "review start"],
|
|
625
673
|
["gentle-ai review finalize --lineage lineage-secret --payload '{\"secret\":true}'", "review finalize"],
|
|
@@ -673,7 +721,7 @@ test("quiet tool rendering displays only finite safe Gentle AI operation paths",
|
|
|
673
721
|
routineRenderContext({ args: { command } }),
|
|
674
722
|
),
|
|
675
723
|
);
|
|
676
|
-
assert.equal(cardTitle(rendered),
|
|
724
|
+
assert.equal(cardTitle(rendered), path === "review" ? "🌹 rdd running" : `🌹 rdd running · ${path.slice("review ".length)}`, command);
|
|
677
725
|
assert.doesNotMatch(rendered, /change-123|private|secret|lineage|sha256|result\.json|incident\.json/);
|
|
678
726
|
}
|
|
679
727
|
});
|
|
@@ -682,22 +730,20 @@ test("quiet tool rendering covers version and future standalone Gentle AI comman
|
|
|
682
730
|
const { pi, tools } = createPi();
|
|
683
731
|
withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
|
|
684
732
|
const tool = tools.get("bash");
|
|
685
|
-
const rose = "🌹︎";
|
|
686
733
|
const cases = [
|
|
687
|
-
["gentle-ai version", "version"],
|
|
688
|
-
["gentle-ai future-command --authorization-root /repo/private", "command"],
|
|
689
|
-
["gentle-ai
|
|
690
|
-
["gentle-ai
|
|
691
|
-
["
|
|
692
|
-
["C:\\package\\.gentle-ai\\v2.2.0\\gentle-ai.exe future-command --root C:\\private", "command"],
|
|
734
|
+
["gentle-ai version", "gentle-ai running · version"],
|
|
735
|
+
["gentle-ai future-command --authorization-root /repo/private", "gentle-ai running · command"],
|
|
736
|
+
["gentle-ai review future-operation --path /repo/private", "rdd running"],
|
|
737
|
+
["/package/.gentle-ai/v2.2.0/gentle-ai version", "gentle-ai running · version"],
|
|
738
|
+
["C:\\package\\.gentle-ai\\v2.2.0\\gentle-ai.exe future-command --root C:\\private", "gentle-ai running · command"],
|
|
693
739
|
] as const;
|
|
694
740
|
|
|
695
741
|
for (const [command, path] of cases) {
|
|
696
742
|
const rendered = renderToString(
|
|
697
743
|
tool.renderCall({ command }, passthroughTheme, routineRenderContext({ args: { command } })),
|
|
698
744
|
);
|
|
699
|
-
assert.equal(cardTitle(rendered),
|
|
700
|
-
assert.doesNotMatch(rendered, /authorization-root|
|
|
745
|
+
assert.equal(cardTitle(rendered), `🌹 ${path}`, command);
|
|
746
|
+
assert.doesNotMatch(rendered, /authorization-root|private|C:\\\\private/);
|
|
701
747
|
}
|
|
702
748
|
});
|
|
703
749
|
|
|
@@ -724,13 +770,13 @@ test("quiet tool rendering recognizes exact quoted, escaped, Windows, and comman
|
|
|
724
770
|
|
|
725
771
|
for (const command of routineCommands) {
|
|
726
772
|
const call = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
|
|
727
|
-
assert.equal(cardTitle(call), "
|
|
773
|
+
assert.equal(cardTitle(call), "🌹 gentle-ai running · version", command);
|
|
728
774
|
}
|
|
729
775
|
|
|
730
776
|
for (const command of ["'gentle-ai-copy' version", '"gentle-ai-copy.exe" version', "'gentle-ai' version && echo done"] as const) {
|
|
731
777
|
const call = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
|
|
732
778
|
assert.equal(call.trimEnd(), `$ ${command}`);
|
|
733
|
-
assert.doesNotMatch(call,
|
|
779
|
+
assert.doesNotMatch(call, /🌹/);
|
|
734
780
|
}
|
|
735
781
|
});
|
|
736
782
|
|
|
@@ -743,35 +789,33 @@ test("quiet tool rendering recognizes only the exact resolved dev binary", () =>
|
|
|
743
789
|
});
|
|
744
790
|
const tool = tools.get("bash");
|
|
745
791
|
const cases = [
|
|
746
|
-
[`${devPath} review validate --gate pre-commit --cwd /repo/private`, "
|
|
747
|
-
[`'${devPath}' review status --lineage secret`, "
|
|
748
|
-
[`"${devPath}" version`, "version"],
|
|
749
|
-
[`env FOO='a b' ${devPath} review capabilities`, "
|
|
750
|
-
[`command -- "${devPath}" sdd-status hidden`, "sdd status"],
|
|
792
|
+
[`${devPath} review validate --gate pre-commit --cwd /repo/private`, "rdd running · validate pre commit"],
|
|
793
|
+
[`'${devPath}' review status --lineage secret`, "rdd running · status"],
|
|
794
|
+
[`"${devPath}" version`, "gentle-ai running · version"],
|
|
795
|
+
[`env FOO='a b' ${devPath} review capabilities`, "rdd running · capabilities"],
|
|
751
796
|
] as const;
|
|
752
797
|
for (const [command, operationPath] of cases) {
|
|
753
798
|
const call = renderToString(tool.renderCall({ command }, statusTheme, routineRenderContext({ args: { command } })));
|
|
754
|
-
assert.equal(cardTitle(call),
|
|
799
|
+
assert.equal(cardTitle(call), `🌹 ${operationPath}`, command); assert.equal(cardTone(call), "warning", command);
|
|
755
800
|
assert.doesNotMatch(call, /gentle-ai-main|private|secret|hidden/);
|
|
756
801
|
}
|
|
757
802
|
const command = `${devPath} review status --prompt hidden-prompt --lineage lineage-secret --body private-body`;
|
|
758
803
|
const lifecycleContext = routineRenderContext({ args: { command }, state: {} });
|
|
759
|
-
assert.match(renderToString(tool.renderCall({ command }, passthroughTheme, lifecycleContext)), /
|
|
804
|
+
assert.match(renderToString(tool.renderCall({ command }, passthroughTheme, lifecycleContext)), /rdd running · status/);
|
|
760
805
|
devPath = "/new/dev/gentle-ai-main";
|
|
761
806
|
const refreshedCommand = `${devPath} review status --token token-secret`;
|
|
762
|
-
assert.match(renderToString(tool.renderCall({ command: refreshedCommand }, passthroughTheme, routineRenderContext({ args: { command: refreshedCommand } }))), /
|
|
807
|
+
assert.match(renderToString(tool.renderCall({ command: refreshedCommand }, passthroughTheme, routineRenderContext({ args: { command: refreshedCommand } }))), /rdd running · status/);
|
|
763
808
|
assertGenericBash(tool, cases[2][0]);
|
|
764
809
|
const text = "error: private failure\nlineage=secret body=hidden\x1b[31m";
|
|
765
810
|
const collapsed = renderToolResult(tool, textResult(text), { expanded: false, isPartial: false, isError: true }, lifecycleContext);
|
|
766
811
|
const expanded = renderToolResult(tool, textResult(text), { expanded: true, isPartial: false, isError: true }, lifecycleContext);
|
|
767
812
|
const refreshed = renderToolResult(tool, textResult("result-secret"), { expanded: false, isPartial: false }, { args: { command: refreshedCommand } });
|
|
768
813
|
const hint = keyHint("app.tools.expand", "to expand");
|
|
769
|
-
assert.match(cardBody(collapsed),
|
|
814
|
+
assert.match(cardBody(collapsed), /private failure/);
|
|
770
815
|
assert.equal(cardBody(collapsed).split(hint).length - 1, 0);
|
|
771
|
-
assert.doesNotMatch(collapsed, /private|lineage|secret|hidden|error/);
|
|
772
816
|
assert.match(expanded, /private failure|lineage=secret body=hidden/);
|
|
773
817
|
assert.doesNotMatch(cardBody(expanded), /to expand|\x1b\[/);
|
|
774
|
-
assert.
|
|
818
|
+
assert.match(refreshed, /result-secret/);
|
|
775
819
|
assert.ok(resolutions > 1);
|
|
776
820
|
});
|
|
777
821
|
test("quiet tool rendering keeps unregistered dev lookalikes and composed calls generic", () => {
|
|
@@ -792,7 +836,7 @@ test("quiet tool rendering keeps unregistered dev lookalikes and composed calls
|
|
|
792
836
|
assertGenericBash(throwing!.get("bash"), unresolved);
|
|
793
837
|
assertGenericBash(registeredQuietToolsWithResolver(() => ({ path: "relative/gentle-ai-main" })).get("bash"), unresolved);
|
|
794
838
|
});
|
|
795
|
-
test("quiet tool rendering
|
|
839
|
+
test("quiet tool rendering previews routine partial content while the header owns state", () => {
|
|
796
840
|
const { pi, tools } = createPi();
|
|
797
841
|
withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
|
|
798
842
|
const tool = tools.get("bash");
|
|
@@ -832,12 +876,12 @@ test("quiet tool rendering hides the routine partial result because the header o
|
|
|
832
876
|
),
|
|
833
877
|
);
|
|
834
878
|
|
|
835
|
-
assert.
|
|
879
|
+
assert.equal(cardBody(partial), "running");
|
|
836
880
|
assert.equal(cardBody(partial).split(expandHint).length - 1, 0);
|
|
837
|
-
assert.match(partialExpanded, /"status":"running"/);
|
|
881
|
+
assert.match(partialExpanded, /"status": "running"/);
|
|
838
882
|
assert.doesNotMatch(partialExpanded, /to expand/);
|
|
839
|
-
assert.match(cardBody(partialFailure),
|
|
840
|
-
assert.
|
|
883
|
+
assert.match(cardBody(partialFailure), /authority unavailable/);
|
|
884
|
+
assert.equal(cardBody(completed), "running");
|
|
841
885
|
|
|
842
886
|
const partialExpandedFailure = renderToString(
|
|
843
887
|
tool.renderResult(
|
|
@@ -851,62 +895,7 @@ test("quiet tool rendering hides the routine partial result because the header o
|
|
|
851
895
|
assert.doesNotMatch(cardBody(partialExpandedFailure), /\x1b\[/);
|
|
852
896
|
});
|
|
853
897
|
|
|
854
|
-
test("quiet tool rendering
|
|
855
|
-
const { pi, tools } = createPi();
|
|
856
|
-
withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
|
|
857
|
-
const tool = tools.get("bash");
|
|
858
|
-
const command = "gentle-ai sdd-attempt grant --authorization-root /repo/root --authorization-root /other/root --change secret-change\x1b[31m";
|
|
859
|
-
|
|
860
|
-
const initial = tool.renderCall(
|
|
861
|
-
{ command },
|
|
862
|
-
statusTheme,
|
|
863
|
-
routineRenderContext({ args: { command } }),
|
|
864
|
-
);
|
|
865
|
-
const running = tool.renderCall(
|
|
866
|
-
{ command },
|
|
867
|
-
statusTheme,
|
|
868
|
-
routineRenderContext({ args: { command }, executionStarted: true, lastComponent: initial }),
|
|
869
|
-
);
|
|
870
|
-
const completed = tool.renderCall(
|
|
871
|
-
{ command },
|
|
872
|
-
statusTheme,
|
|
873
|
-
routineRenderContext({ args: { command }, executionStarted: true, isPartial: false, lastComponent: running }),
|
|
874
|
-
);
|
|
875
|
-
const failed = tool.renderCall(
|
|
876
|
-
{ command },
|
|
877
|
-
statusTheme,
|
|
878
|
-
routineRenderContext({ args: { command }, executionStarted: true, isPartial: false, isError: true, lastComponent: completed }),
|
|
879
|
-
);
|
|
880
|
-
|
|
881
|
-
assert.strictEqual(initial, running);
|
|
882
|
-
assert.strictEqual(running, completed);
|
|
883
|
-
assert.strictEqual(completed, failed);
|
|
884
|
-
const rendered = renderToString(failed);
|
|
885
|
-
assert.equal(cardTitle(rendered), "🌹︎ Gentle AI · failed · sdd attempt grant · 2 roots"); assert.equal(cardTone(rendered), "error");
|
|
886
|
-
assert.doesNotMatch(rendered.split(keyHint("app.tools.expand", "to expand")).join(""), /authorization-root|secret-change|repo\/root|other\/root|audit:|\x1b\[/);
|
|
887
|
-
});
|
|
888
|
-
|
|
889
|
-
test("quiet tool rendering counts grant authorization roots without rendering values", () => {
|
|
890
|
-
const { pi, tools } = createPi();
|
|
891
|
-
withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
|
|
892
|
-
const tool = tools.get("bash");
|
|
893
|
-
const cases = [
|
|
894
|
-
["gentle-ai sdd-attempt grant --change change", "sdd attempt grant"],
|
|
895
|
-
["gentle-ai sdd-attempt grant --authorization-root /repo/root", "sdd attempt grant · 1 root"],
|
|
896
|
-
["gentle-ai sdd-attempt grant --authorization-root=/repo/root", "sdd attempt grant · 1 root"],
|
|
897
|
-
["gentle-ai sdd-attempt grant --authorization-root --change change", "sdd attempt grant"],
|
|
898
|
-
["gentle-ai sdd-attempt grant --authorization-root /one --authorization-root=/two --authorization-root /three", "sdd attempt grant · 3 roots"],
|
|
899
|
-
["gentle-ai sdd-attempt grant --authorization-root= --authorization-root /one", "sdd attempt grant · 1 root"],
|
|
900
|
-
] as const;
|
|
901
|
-
|
|
902
|
-
for (const [command, expected] of cases) {
|
|
903
|
-
const rendered = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
|
|
904
|
-
assert.equal(cardTitle(rendered), `🌹︎ Gentle AI · running · ${expected}`, command);
|
|
905
|
-
assert.doesNotMatch(rendered, /authorization-root|\/repo\/root|\/one|\/two|\/three|change/);
|
|
906
|
-
}
|
|
907
|
-
});
|
|
908
|
-
|
|
909
|
-
test("quiet tool rendering hides invocation secrets from collapsed Gentle AI calls and results", () => {
|
|
898
|
+
test("quiet tool rendering hides invocation arguments but previews result content without redaction", () => {
|
|
910
899
|
const { pi, tools } = createPi();
|
|
911
900
|
withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
|
|
912
901
|
const tool = tools.get("bash");
|
|
@@ -921,11 +910,11 @@ test("quiet tool rendering hides invocation secrets from collapsed Gentle AI cal
|
|
|
921
910
|
),
|
|
922
911
|
);
|
|
923
912
|
|
|
924
|
-
assert.equal(cardTitle(call), "
|
|
925
|
-
assert.match(cardBody(collapsed),
|
|
913
|
+
assert.equal(cardTitle(call), "🌹 rdd running · finalize");
|
|
914
|
+
assert.match(cardBody(collapsed), /audit:/);
|
|
926
915
|
for (const forbidden of ["hidden-prompt", "lineage-secret", "private-body", "/private/root", "audit:"]) {
|
|
927
916
|
assert.doesNotMatch(call, new RegExp(forbidden));
|
|
928
|
-
assert.
|
|
917
|
+
assert.match(collapsed, new RegExp(forbidden), "result preview is not secret redaction");
|
|
929
918
|
}
|
|
930
919
|
});
|
|
931
920
|
|
|
@@ -952,7 +941,7 @@ test("quiet tool rendering keeps shell compositions, expansions, and lookalikes
|
|
|
952
941
|
tool.renderResult(textResult("original command output"), { expanded: true, isPartial: false }, passthroughTheme, { args: { command } }),
|
|
953
942
|
);
|
|
954
943
|
assert.equal(call.trimEnd(), `$ ${command}`);
|
|
955
|
-
assert.doesNotMatch(call,
|
|
944
|
+
assert.doesNotMatch(call, /🌹/);
|
|
956
945
|
assert.match(output, /original command output/);
|
|
957
946
|
}
|
|
958
947
|
});
|
|
@@ -977,11 +966,11 @@ test("quiet Bash previews semantic lines for generic JSON output", () => {
|
|
|
977
966
|
const collapsed = renderToolResult(tool, textResult(object), { expanded: false, isPartial: false }, { args: { command } });
|
|
978
967
|
const expanded = renderToolResult(tool, textResult(object), { expanded: true, isPartial: false }, { args: { command } });
|
|
979
968
|
assert.equal(call.trimEnd(), `$ ${command}`);
|
|
980
|
-
assert.doesNotMatch(call,
|
|
969
|
+
assert.doesNotMatch(call, /🌹/);
|
|
981
970
|
assert.doesNotMatch(collapsed, /^\n\s*[}\]]/);
|
|
982
971
|
const expandHint = keyHint("app.tools.expand", "to expand");
|
|
983
|
-
assert.equal(collapsed.split(expandHint).length - 1,
|
|
984
|
-
assert.equal(expanded,
|
|
972
|
+
assert.equal(collapsed.split(expandHint).length - 1, 0);
|
|
973
|
+
assert.equal(expanded, object);
|
|
985
974
|
assert.doesNotMatch(expanded, /to expand/);
|
|
986
975
|
});
|
|
987
976
|
|
|
@@ -1044,19 +1033,21 @@ test("quiet tool rendering limits collapsed visual rows at the actual width", ()
|
|
|
1044
1033
|
const tools = registeredQuietTools();
|
|
1045
1034
|
const bash = tools.get("bash");
|
|
1046
1035
|
const hint = keyHint("app.tools.expand", "to expand");
|
|
1047
|
-
|
|
1036
|
+
// Twelve content columns inside the four-column card sides.
|
|
1037
|
+
const narrowWidth = 16;
|
|
1048
1038
|
const cases = [
|
|
1049
|
-
|
|
1039
|
+
// Preview rows plus the closing rule; a partial result adds its label row.
|
|
1040
|
+
[textResult("x".repeat(80)), { expanded: false, isPartial: true }, { args: { command: "printf output" } }, 5],
|
|
1050
1041
|
[textResult("界".repeat(40)), { expanded: false, isPartial: false }, { args: { command: "printf output" } }, 4],
|
|
1051
1042
|
[textResult("e\u0301".repeat(40)), { expanded: false, isPartial: false, isError: true }, { args: { command: "false" }, isError: true }, 4],
|
|
1052
1043
|
] as const;
|
|
1053
1044
|
for (const [result, options, context, maxRows] of cases) {
|
|
1054
|
-
const lines =
|
|
1045
|
+
const lines = frameLines(bash.renderResult(result, options, passthroughTheme, context), narrowWidth);
|
|
1055
1046
|
assert.ok(lines.length <= maxRows, `expected at most ${maxRows} rows, got ${lines.length}`);
|
|
1056
1047
|
assert.ok(lines.every((line) => visibleWidth(line) <= narrowWidth));
|
|
1057
1048
|
}
|
|
1058
|
-
const completed =
|
|
1059
|
-
assert.equal(completed.filter((line) => line.includes(hint)).length,
|
|
1049
|
+
const completed = frameLines(bash.renderResult(textResult("😀".repeat(40)), { expanded: false, isPartial: false }, passthroughTheme, { args: { command: "printf output" } }), narrowWidth);
|
|
1050
|
+
assert.equal(completed.filter((line) => line.includes(hint)).length, 0);
|
|
1060
1051
|
assert.ok(completed.every((line) => visibleWidth(line) <= narrowWidth));
|
|
1061
1052
|
});
|
|
1062
1053
|
|
|
@@ -1069,8 +1060,8 @@ test("quiet tool rendering classifies quoted and escaped literal shell metachara
|
|
|
1069
1060
|
]) {
|
|
1070
1061
|
const call = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
|
|
1071
1062
|
const collapsed = renderToolResult(tool, textResult("private output"), { expanded: false, isPartial: false }, { args: { command } });
|
|
1072
|
-
assert.match(call,
|
|
1073
|
-
assert.
|
|
1063
|
+
assert.match(call, /🌹 rdd running · status/, command);
|
|
1064
|
+
assert.match(collapsed, /private output/, command);
|
|
1074
1065
|
}
|
|
1075
1066
|
});
|
|
1076
1067
|
|
|
@@ -1102,9 +1093,9 @@ test("quiet tool rendering recognizes a quoted exact dev override path containin
|
|
|
1102
1093
|
const command = `"${devPath}" review status "literal \\$|#;"`;
|
|
1103
1094
|
const call = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
|
|
1104
1095
|
const collapsed = renderToolResult(tool, textResult("private result"), { expanded: false, isPartial: false }, { args: { command } });
|
|
1105
|
-
assert.equal(cardTitle(call), "
|
|
1096
|
+
assert.equal(cardTitle(call), "🌹 rdd running · status");
|
|
1106
1097
|
assert.doesNotMatch(call, /\/opt\/Gentle AI\/gentle-ai|literal/);
|
|
1107
|
-
assert.
|
|
1098
|
+
assert.match(collapsed, /private result/);
|
|
1108
1099
|
});
|
|
1109
1100
|
|
|
1110
1101
|
test("quiet tool rendering bounds and sanitizes partial text without a completion hint", () => {
|
|
@@ -1142,20 +1133,20 @@ test("quiet tool rendering bounds completed previews, errors, and edit/write sum
|
|
|
1142
1133
|
const rendered = renderToolResult(tools.get(toolName), textResult(text), { expanded: false, isPartial: false }, context);
|
|
1143
1134
|
assert.doesNotMatch(rendered, new RegExp(hidden));
|
|
1144
1135
|
assert.match(rendered, new RegExp(visible));
|
|
1145
|
-
assert.
|
|
1136
|
+
assert.doesNotMatch(rendered, /to expand/);
|
|
1146
1137
|
}
|
|
1147
1138
|
|
|
1148
1139
|
for (const toolName of ["read", "bash", "grep", "find", "ls", "edit", "write"] as const) {
|
|
1149
1140
|
const rendered = renderToolResult(tools.get(toolName), textResult("error one\nerror two\nerror three\nerror four"), { expanded: false, isPartial: false, isError: true }, { args: toolName === "bash" ? { command: "false" } : {} });
|
|
1150
1141
|
assert.doesNotMatch(rendered, /error one/);
|
|
1151
1142
|
assert.match(rendered, /error two\nerror three\nerror four/);
|
|
1152
|
-
assert.
|
|
1143
|
+
assert.doesNotMatch(rendered, /to expand/);
|
|
1153
1144
|
}
|
|
1154
1145
|
|
|
1155
1146
|
const edit = renderToolResult(tools.get("edit"), textResult("Applied", { diff: "@@\n-old\n+new" }), { expanded: false, isPartial: false }, { args: { path: "file.ts", edits: [] } });
|
|
1156
1147
|
const write = renderToolResult(tools.get("write"), textResult("Successfully wrote 4 bytes\nbody"), { expanded: false, isPartial: false }, { args: { path: "file.ts", content: "body" } });
|
|
1157
1148
|
assert.match(edit, /\+1 \/ -1/);
|
|
1158
|
-
assert.
|
|
1149
|
+
assert.match(edit, /-old[\s\S]*\+new/);
|
|
1159
1150
|
assert.match(write, /wrote 4 bytes/);
|
|
1160
1151
|
assert.doesNotMatch(write, /body/);
|
|
1161
1152
|
});
|
|
@@ -1184,7 +1175,8 @@ test("quiet tool rendering preserves directional visual preview rows at narrow w
|
|
|
1184
1175
|
const tools = registeredQuietTools();
|
|
1185
1176
|
const bash = tools.get("bash");
|
|
1186
1177
|
const readTool = tools.get("read");
|
|
1187
|
-
|
|
1178
|
+
// Twelve content columns inside the four-column card sides.
|
|
1179
|
+
const narrowWidth = 16;
|
|
1188
1180
|
const renderCollapsed = (
|
|
1189
1181
|
text: string,
|
|
1190
1182
|
options: { expanded: false; isPartial: boolean; isError?: boolean },
|
|
@@ -1213,7 +1205,7 @@ test("quiet tool rendering preserves directional visual preview rows at narrow w
|
|
|
1213
1205
|
assert.match(lines.join("\n"), new RegExp(marker));
|
|
1214
1206
|
}
|
|
1215
1207
|
assert.match(partial[0] ?? "", /… bash/);
|
|
1216
|
-
assert.
|
|
1208
|
+
assert.doesNotMatch(completedBash.join("\n"), /to expand/);
|
|
1217
1209
|
|
|
1218
1210
|
const read = renderRead(`READ-FIRST\n${oldText}\nREAD-NEW`);
|
|
1219
1211
|
const semanticJson = renderCollapsed(
|
|
@@ -1259,8 +1251,8 @@ test("quiet tool rendering fails closed on unquoted shell expansions", () => {
|
|
|
1259
1251
|
const call = renderToString(bash.renderCall({ command }, passthroughTheme, { args: { command } }));
|
|
1260
1252
|
const collapsed = renderToolResult(bash, textResult("dummy-secret"), { expanded: false, isPartial: false }, { args: { command } });
|
|
1261
1253
|
assert.equal(gentleAiRoutineCommand({ command }), "review", command);
|
|
1262
|
-
assert.match(call,
|
|
1263
|
-
assert.
|
|
1254
|
+
assert.match(call, /🌹 rdd running · status/, command);
|
|
1255
|
+
assert.match(collapsed, /dummy-secret/, command);
|
|
1264
1256
|
}
|
|
1265
1257
|
});
|
|
1266
1258
|
|
|
@@ -1275,9 +1267,9 @@ test("quiet tool rendering redacts only exact package-local executable paths wit
|
|
|
1275
1267
|
const call = renderToString(bash.renderCall({ command }, passthroughTheme, { args: { command } }));
|
|
1276
1268
|
const collapsed = renderToolResult(bash, textResult("dummy-secret"), { expanded: false, isPartial: false }, { args: { command } });
|
|
1277
1269
|
assert.equal(gentleAiRoutineCommand({ command }), "review", command);
|
|
1278
|
-
assert.match(call,
|
|
1270
|
+
assert.match(call, /🌹 rdd running · status/, command);
|
|
1279
1271
|
assert.doesNotMatch(call, /Gentle Package|secret/, command);
|
|
1280
|
-
assert.
|
|
1272
|
+
assert.match(collapsed, /dummy-secret/, command);
|
|
1281
1273
|
}
|
|
1282
1274
|
|
|
1283
1275
|
for (const command of [
|
|
@@ -1304,8 +1296,8 @@ test("quiet tool rendering respects command and env wrapper order", () => {
|
|
|
1304
1296
|
const call = renderToString(bash.renderCall({ command }, passthroughTheme, { args: { command } }));
|
|
1305
1297
|
const collapsed = renderToolResult(bash, textResult("dummy-secret"), { expanded: false, isPartial: false }, { args: { command } });
|
|
1306
1298
|
assert.equal(gentleAiRoutineCommand({ command }), "review", command);
|
|
1307
|
-
assert.match(call,
|
|
1308
|
-
assert.
|
|
1299
|
+
assert.match(call, /🌹 rdd running · status/, command);
|
|
1300
|
+
assert.match(collapsed, /dummy-secret/, command);
|
|
1309
1301
|
}
|
|
1310
1302
|
|
|
1311
1303
|
const command = "env FOO=bar command -- gentle-ai review status";
|
|
@@ -1325,20 +1317,20 @@ test("quiet tool rendering lets a final result promote a replayed call card to i
|
|
|
1325
1317
|
const replayed = routineRenderContext({ args: { command }, state, argsComplete: false, executionStarted: false, isPartial: false, invalidate: () => { invalidations += 1; } });
|
|
1326
1318
|
|
|
1327
1319
|
const call = tool.renderCall({ command }, statusTheme, replayed);
|
|
1328
|
-
assert.equal(cardTitle(renderToString(call)), "
|
|
1320
|
+
assert.equal(cardTitle(renderToString(call)), "🌹 rdd preparing · status");
|
|
1329
1321
|
renderToString(tool.renderResult(textResult("done"), { expanded: false, isPartial: false }, statusTheme, replayed));
|
|
1330
1322
|
assert.equal(invalidations, 0, "never reentrant");
|
|
1331
1323
|
await flush();
|
|
1332
1324
|
assert.equal(invalidations, 1);
|
|
1333
1325
|
const promoted = tool.renderCall({ command }, statusTheme, { ...replayed, lastComponent: call });
|
|
1334
1326
|
assert.strictEqual(promoted, call);
|
|
1335
|
-
assert.equal(cardTitle(renderToString(promoted)), "
|
|
1327
|
+
assert.equal(cardTitle(renderToString(promoted)), "🌹 rdd status");
|
|
1336
1328
|
assert.equal(cardTone(renderToString(promoted)), "success");
|
|
1337
1329
|
|
|
1338
1330
|
renderToString(tool.renderResult(textResult("boom"), { expanded: false, isPartial: false }, statusTheme, { ...replayed, isError: true }));
|
|
1339
1331
|
await flush();
|
|
1340
1332
|
assert.equal(invalidations, 2);
|
|
1341
|
-
assert.equal(cardTitle(renderToString(tool.renderCall({ command }, statusTheme, { ...replayed, lastComponent: call }))), "
|
|
1333
|
+
assert.equal(cardTitle(renderToString(tool.renderCall({ command }, statusTheme, { ...replayed, lastComponent: call }))), "🌹 rdd failed · status");
|
|
1342
1334
|
renderToString(tool.renderResult(textResult("boom"), { expanded: false, isPartial: false }, statusTheme, { ...replayed, isError: true }));
|
|
1343
1335
|
await flush();
|
|
1344
1336
|
assert.equal(invalidations, 2, "an unchanged outcome must not request another render");
|
|
@@ -1353,11 +1345,244 @@ test("quiet tool rendering puts the expand key in the finished call card's top r
|
|
|
1353
1345
|
assert.equal(cardHint(running), undefined, "a running call has nothing to expand yet");
|
|
1354
1346
|
const completed = renderToString(tool.renderCall({ command }, passthroughTheme, routineRenderContext({ args: { command }, executionStarted: true, isPartial: false, expanded: false })));
|
|
1355
1347
|
assert.match(cardHint(completed) ?? "", /to expand$/);
|
|
1356
|
-
assert.equal(cardTitle(completed), "
|
|
1348
|
+
assert.equal(cardTitle(completed), "🌹 rdd status");
|
|
1357
1349
|
const expanded = renderToString(tool.renderCall({ command }, passthroughTheme, routineRenderContext({ args: { command }, executionStarted: true, isPartial: false, expanded: true })));
|
|
1358
1350
|
assert.match(cardHint(expanded) ?? "", /to collapse$/);
|
|
1359
1351
|
const collapsedResult = renderToolResult(tool, textResult("{\"next_transition\":\"stop\"}"), { expanded: false, isPartial: false }, { args: { command } });
|
|
1360
|
-
assert.equal(cardBody(collapsedResult), "
|
|
1361
|
-
assert.equal(cardBody(renderToolResult(tool, textResult("a\nb\nc"), { expanded: false, isPartial: false }, { args: { command } })), "
|
|
1352
|
+
assert.equal(cardBody(collapsedResult), "stop");
|
|
1353
|
+
assert.equal(cardBody(renderToolResult(tool, textResult("a\nb\nc"), { expanded: false, isPartial: false }, { args: { command } })), "a\nb\nc");
|
|
1362
1354
|
assert.match(collapsedResult.split("\n").pop() ?? "", /╰─+╯$/);
|
|
1363
1355
|
});
|
|
1356
|
+
|
|
1357
|
+
test("long finished quiet headings retain the applicable expansion hint and complete command", () => {
|
|
1358
|
+
const tool = registeredQuietTools().get("bash");
|
|
1359
|
+
const command = "printf alpha bravo charlie delta echo foxtrot golf hotel india juliet kilo lima mike november oscar papa quebec romeo sierra tango";
|
|
1360
|
+
const args = { command };
|
|
1361
|
+
for (const expanded of [false, true]) {
|
|
1362
|
+
const hint = keyHint("app.tools.expand", expanded ? "to collapse" : "to expand").replace(/\x1b\[[\d;]*m/g, "");
|
|
1363
|
+
const ctx = routineRenderContext({ args, executionStarted: true, isPartial: false, expanded });
|
|
1364
|
+
for (const width of [80, 120]) {
|
|
1365
|
+
const rows = frameLines(tool.renderCall(args, passthroughTheme, ctx), width);
|
|
1366
|
+
assert.ok(rows[0].endsWith(` ${hint} ╮`), `width ${width}: applicable hint must survive heading wrapping`);
|
|
1367
|
+
assert.ok(rows.length > 1);
|
|
1368
|
+
assert.ok(rows.every((row) => visibleWidth(row) <= width));
|
|
1369
|
+
const first = rows[0].slice("╭─ $ ".length).split(" ─")[0];
|
|
1370
|
+
const rest = rows.slice(1).map((row) => row.replace(/^│ /, "").replace(/ +│$/, ""));
|
|
1371
|
+
assert.equal([first, ...rest].join(" ").replace(/\s+/g, " ").trim(), `bash $ ${command}`);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
test("quiet components repaint after theme invalidation and retain transparent error frames", (t) => {
|
|
1377
|
+
const found = cardStyle();
|
|
1378
|
+
t.after(() => setCardStyle(found));
|
|
1379
|
+
setCardStyle(CARD_STYLE.NEON);
|
|
1380
|
+
const bash = registeredQuietTools().get("bash");
|
|
1381
|
+
let paint = "\x1b[31m";
|
|
1382
|
+
const theme = { bold: (text: string) => text, fg: (_role: string, text: string) => `${paint}${text}\x1b[0m`, bg: (_role: string, text: string) => `\x1b[44m${text}\x1b[49m` };
|
|
1383
|
+
const args = { command: "printf failure" };
|
|
1384
|
+
const context = routineRenderContext({ args, isError: true, isPartial: false });
|
|
1385
|
+
const components = [bash.renderCall(args, theme, context), bash.renderResult(textResult("denied\nreason"), { expanded: false, isPartial: false }, theme, context)];
|
|
1386
|
+
const first = components.flatMap((component) => component.render(80)).join("\n");
|
|
1387
|
+
paint = "\x1b[35m";
|
|
1388
|
+
for (const component of components) component.invalidate();
|
|
1389
|
+
const changed = components.flatMap((component) => component.render(80)).join("\n");
|
|
1390
|
+
assert.match(first, /\x1b\[31m/);
|
|
1391
|
+
assert.doesNotMatch(changed, /\x1b\[31m|\x1b\[44m/);
|
|
1392
|
+
assert.match(changed, /\x1b\[35m/);
|
|
1393
|
+
assert.equal(first.replace(/\x1b\[[\d;]*m/g, ""), changed.replace(/\x1b\[[\d;]*m/g, ""));
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
test("six quiet cards and standalone Bash fixture retain distinct identity and bounded results", () => {
|
|
1397
|
+
const tools = registeredQuietTools();
|
|
1398
|
+
for (const [name, glyph] of [["read", "≡"], ["bash", "$"], ["grep", "⌕"], ["find", "⌖"], ["ls", "☷"], ["edit", "✎"], ["write", "+"]] as const) {
|
|
1399
|
+
const tool = tools.get(name);
|
|
1400
|
+
const args = { command: "printf hello", path: "src/example.ts", pattern: "needle" };
|
|
1401
|
+
const ctx = routineRenderContext({ args, isPartial: false, executionStarted: true });
|
|
1402
|
+
const header = frameLines(tool.renderCall(args, passthroughTheme, ctx), 120).join("\n");
|
|
1403
|
+
assert.ok(header.startsWith(`╭─ ${glyph} ${name} `), `${name} retains its distinct icon and actual identity`);
|
|
1404
|
+
assert.match(header, /to expand/);
|
|
1405
|
+
const value = textResult("useful row\nsecond row", name === "edit" ? { diff: "-before\n+after" } : undefined);
|
|
1406
|
+
const body = frameLines(tool.renderResult(value, { expanded: false, isPartial: false }, passthroughTheme, ctx), 120).join("\n");
|
|
1407
|
+
if (name !== "write") assert.match(body, name === "edit" ? /before|after/ : /useful row/);
|
|
1408
|
+
for (const width of [0, 1, 2, 3, 4, 5, 6, 7, 8, 24]) {
|
|
1409
|
+
const rows = frameLines(tool.renderResult(textResult("界e\u0301🌹".repeat(80)), { expanded: false, isPartial: false }, passthroughTheme, ctx), width);
|
|
1410
|
+
assert.ok(rows.length <= 4);
|
|
1411
|
+
assert.ok(rows.every((row) => visibleWidth(row) <= width));
|
|
1412
|
+
assert.equal(rows.filter((row) => row.startsWith("╰")).length, width ? 1 : 0);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
test("quiet tool calls draw a rounded petal card in the lifecycle tone", () => {
|
|
1418
|
+
const tools = registeredQuietTools();
|
|
1419
|
+
const bash = tools.get("bash");
|
|
1420
|
+
const args = { command: "printf output" };
|
|
1421
|
+
const width = 30;
|
|
1422
|
+
|
|
1423
|
+
const header = bash.renderCall(args, passthroughTheme, routineRenderContext({ args, isPartial: false })).render(width);
|
|
1424
|
+
assert.deepEqual(header, [`╭─ $ bash $ printf output ${"─".repeat(3)}╮`]);
|
|
1425
|
+
const running = bash.renderCall(args, statusTheme, routineRenderContext({ args, executionStarted: true, isPartial: true })).render(width);
|
|
1426
|
+
assert.match(running[0] ?? "", /^<warning>╭<\/warning><warning>─ <\/warning><warning>\$ (?:<toolTitle>)?bash/);
|
|
1427
|
+
assert.match(bash.renderCall(args, statusTheme, routineRenderContext({ args, isPartial: false, isError: true })).render(width)[0] ?? "", /^<error>╭<\/error>/);
|
|
1428
|
+
|
|
1429
|
+
const expanded = bash.renderResult(textResult("alpha\nbeta"), { expanded: true, isPartial: false }, passthroughTheme, { args }).render(width);
|
|
1430
|
+
assert.deepEqual(expanded, [
|
|
1431
|
+
`│ alpha${" ".repeat(21)} │`,
|
|
1432
|
+
`│ beta${" ".repeat(22)} │`,
|
|
1433
|
+
`╰${"─".repeat(28)}╯`,
|
|
1434
|
+
]);
|
|
1435
|
+
const failed = bash.renderResult(textResult("boom"), { expanded: true, isPartial: false, isError: true }, statusTheme, { args, isError: true }).render(width);
|
|
1436
|
+
assert.match(failed.at(-1) ?? "", /^<error>╰<\/error><error>─+╯<\/error>$/);
|
|
1437
|
+
assert.ok(failed.slice(0, -1).every((line) => line.startsWith("<error>│</error> ")), failed.join("\n"));
|
|
1438
|
+
|
|
1439
|
+
const empty = tools.get("grep").renderResult(textResult("No matches found"), { expanded: false, isPartial: false }, passthroughTheme, {}).render(width);
|
|
1440
|
+
assert.deepEqual(empty, [`╰${"─".repeat(28)}╯`], "an empty result still closes the card");
|
|
1441
|
+
|
|
1442
|
+
// The expand key rides the top rule once the call finished, like the Gentle AI card.
|
|
1443
|
+
const wide = 80;
|
|
1444
|
+
const expandKey = keyHint("app.tools.expand", "to expand").replace(/\x1b\[[0-9;]*m/g, "");
|
|
1445
|
+
const finishedTop = bash.renderCall(args, passthroughTheme, routineRenderContext({ args, executionStarted: true, isPartial: false })).render(wide)[0] ?? "";
|
|
1446
|
+
assert.ok(finishedTop.endsWith(` ${expandKey} ╮`), finishedTop);
|
|
1447
|
+
const expandedTop = bash.renderCall(args, passthroughTheme, routineRenderContext({ args, executionStarted: true, isPartial: false, expanded: true })).render(wide)[0] ?? "";
|
|
1448
|
+
assert.match(expandedTop, /to collapse ╮$/);
|
|
1449
|
+
const runningTop = bash.renderCall(args, passthroughTheme, routineRenderContext({ args, executionStarted: true, isPartial: true })).render(wide)[0] ?? "";
|
|
1450
|
+
assert.ok(!runningTop.includes("to expand"), runningTop);
|
|
1451
|
+
const collapsedBody = bash.renderResult(textResult("alpha\nbeta"), { expanded: false, isPartial: false }, passthroughTheme, { args }).render(wide);
|
|
1452
|
+
assert.ok(!collapsedBody.some((line) => line.includes("to expand")), "the body no longer carries the expand key");
|
|
1453
|
+
|
|
1454
|
+
for (const narrow of [1, 2, 3, 4, 5, 8]) {
|
|
1455
|
+
const lines = [
|
|
1456
|
+
...bash.renderCall({ command: "printf " + "x".repeat(80) }, passthroughTheme, routineRenderContext({ args, isPartial: false })).render(narrow),
|
|
1457
|
+
...bash.renderResult(textResult("😀".repeat(40)), { expanded: true, isPartial: false }, passthroughTheme, { args }).render(narrow),
|
|
1458
|
+
];
|
|
1459
|
+
for (const line of lines) assert.ok(visibleWidth(line) <= narrow, `width ${narrow}: ${JSON.stringify(line)}`);
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1463
|
+
// Pi builds the call component, then the result component once a result
|
|
1464
|
+
// exists, and only then renders them; both share one row state.
|
|
1465
|
+
function piToolRow(tool: any, args: Record<string, unknown>, value: unknown, context: Record<string, unknown>, width: number, theme: object = passthroughTheme): string[] {
|
|
1466
|
+
const call = tool.renderCall(args, theme, context);
|
|
1467
|
+
const body = value ? tool.renderResult(value, { expanded: context.expanded === true, isPartial: context.isPartial === true }, theme, context) : undefined;
|
|
1468
|
+
return [...call.render(width), ...(body?.render(width) ?? [])];
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
test("a running quiet card closes its own frame with one running row until a result exists", () => {
|
|
1472
|
+
const tools = registeredQuietTools();
|
|
1473
|
+
const read = tools.get("read");
|
|
1474
|
+
const args = { path: "a.md" };
|
|
1475
|
+
const pending = () => routineRenderContext({ args, executionStarted: true, isPartial: true, state: {} });
|
|
1476
|
+
const running = piToolRow(read, args, undefined, pending(), 40);
|
|
1477
|
+
assert.deepEqual(running, [
|
|
1478
|
+
`╭─ ≡ read a.md ${"─".repeat(24)}╮`,
|
|
1479
|
+
`│ running…${" ".repeat(28)} │`,
|
|
1480
|
+
`╰${"─".repeat(38)}╯`,
|
|
1481
|
+
]);
|
|
1482
|
+
const painted = piToolRow(read, args, undefined, pending(), 40, statusTheme).join("\n");
|
|
1483
|
+
assert.match(painted, /<muted>running…<\/muted>/);
|
|
1484
|
+
assert.match(painted, /<warning>╰<\/warning>/);
|
|
1485
|
+
for (const width of [0, 1, 2, 3, 4, 5, 6, 7, 8]) {
|
|
1486
|
+
const rows = piToolRow(read, args, undefined, pending(), width);
|
|
1487
|
+
assert.ok(rows.every((row) => visibleWidth(row) <= width), `width ${width}`);
|
|
1488
|
+
// Narrow headings may continue on rows below the top rule; the frame still closes once.
|
|
1489
|
+
assert.equal(rows.filter((row) => row.startsWith("╰")).length, width === 0 ? 0 : 1);
|
|
1490
|
+
if (width > 0) assert.ok(rows.at(-1)!.startsWith("╰"), `width ${width}: ${rows.join("\n")}`);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// A partial result below the call closes the frame instead: one bottom rule, no running row.
|
|
1494
|
+
const bash = tools.get("bash");
|
|
1495
|
+
const bashArgs = { command: "printf output" };
|
|
1496
|
+
const partial = piToolRow(bash, bashArgs, textResult("out"), routineRenderContext({ args: bashArgs, executionStarted: true, isPartial: true, state: {} }), 40);
|
|
1497
|
+
assert.equal(partial.filter((row) => row.startsWith("╭")).length, 1);
|
|
1498
|
+
assert.equal(partial.filter((row) => row.startsWith("╰")).length, 1);
|
|
1499
|
+
assert.match(partial.at(-1)!, /^╰─+╯$/);
|
|
1500
|
+
assert.doesNotMatch(partial.join("\n"), /running…/);
|
|
1501
|
+
|
|
1502
|
+
// A finished call keeps its open top rule; the result closes the card.
|
|
1503
|
+
const finished = piToolRow(read, args, textResult("one"), routineRenderContext({ args, executionStarted: true, isPartial: false, state: {} }), 40);
|
|
1504
|
+
assert.equal(finished.length, 3);
|
|
1505
|
+
assert.match(finished[0]!, /^╭─ ≡ read a\.md .*╮$/);
|
|
1506
|
+
assert.match(finished[1]!, /^│ one +│$/);
|
|
1507
|
+
assert.match(finished[2]!, /^╰─+╯$/);
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1510
|
+
|
|
1511
|
+
|
|
1512
|
+
// Float style: the call and result components of one quiet card read as one
|
|
1513
|
+
// borderless panel. The heading owns the blank row above it; the result owns
|
|
1514
|
+
// the separator below the heading and the blank closing row.
|
|
1515
|
+
const floatToolTheme = {
|
|
1516
|
+
bold(value: string) {
|
|
1517
|
+
return value;
|
|
1518
|
+
},
|
|
1519
|
+
fg(_color: string, value: string) {
|
|
1520
|
+
return `\x1b[38;5;114m${value}\x1b[39m`;
|
|
1521
|
+
},
|
|
1522
|
+
bg(color: string, value: string) {
|
|
1523
|
+
return `\x1b[48;5;${color === "toolErrorBg" ? 52 : color === "toolPendingBg" ? 58 : 22}m${value}\x1b[49m`;
|
|
1524
|
+
},
|
|
1525
|
+
};
|
|
1526
|
+
|
|
1527
|
+
function withFloatCards<T>(run: () => T): T {
|
|
1528
|
+
setCardStyle(CARD_STYLE.FLOAT);
|
|
1529
|
+
try {
|
|
1530
|
+
return run();
|
|
1531
|
+
} finally {
|
|
1532
|
+
setCardStyle(CARD_STYLE.NEON);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
test("float quiet cards render the call and result as one panel with a single separator", () => {
|
|
1537
|
+
const tools = registeredQuietTools();
|
|
1538
|
+
const read = tools.get("read");
|
|
1539
|
+
const context = routineRenderContext({ args: { path: "package.json" }, isPartial: false, executionStarted: true });
|
|
1540
|
+
const width = 60;
|
|
1541
|
+
const rows = withFloatCards(() => [
|
|
1542
|
+
...read.renderCall({ path: "package.json" }, floatToolTheme, context).render(width),
|
|
1543
|
+
...read.renderResult(textResult("{\n \"name\": \"gentle-pi\""), { expanded: false, isPartial: false }, floatToolTheme, context).render(width),
|
|
1544
|
+
]);
|
|
1545
|
+
const plain = rows.map(stripAnsi);
|
|
1546
|
+
for (const line of rows) {
|
|
1547
|
+
assert.equal(visibleWidth(line), width);
|
|
1548
|
+
assert.ok(line.startsWith(" \x1b[48;5;22m"), JSON.stringify(line));
|
|
1549
|
+
assert.ok(line.endsWith("\x1b[49m "), JSON.stringify(line));
|
|
1550
|
+
}
|
|
1551
|
+
assert.doesNotMatch(plain.join("\n"), /[╭╮╰╯│─]/);
|
|
1552
|
+
assert.match(plain[0]!, /^ ▎ +$/);
|
|
1553
|
+
assert.match(plain[1]!, /^ ▎ ≡ read package\.json +.*to expand {4}$/);
|
|
1554
|
+
assert.match(plain[2]!, /^ ▎ +$/);
|
|
1555
|
+
assert.match(plain[3]!, /^ ▎ \{ +$/);
|
|
1556
|
+
assert.match(plain[4]!, /^ ▎ {3}"name": "gentle-pi" +$/);
|
|
1557
|
+
assert.match(plain.at(-1)!, /^ ▎ +$/);
|
|
1558
|
+
assert.equal(plain.length, 6);
|
|
1559
|
+
assert.equal(plain[1]!.indexOf("≡"), plain[3]!.indexOf("{"), "the glyph starts in the body text column");
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
test("float quiet cards close a running call as one panel and add no separator without result rows", () => {
|
|
1563
|
+
const tools = registeredQuietTools();
|
|
1564
|
+
const read = tools.get("read");
|
|
1565
|
+
const running = withFloatCards(() => read.renderCall({ path: "a.md" }, floatToolTheme, routineRenderContext({ args: { path: "a.md" }, state: {} })).render(40)).map(stripAnsi);
|
|
1566
|
+
assert.equal(running.length, 5, "blank row, heading, separator, running row, closing row");
|
|
1567
|
+
assert.match(running[0]!, /^ ▎ +$/);
|
|
1568
|
+
assert.match(running[1]!, /^ ▎ ≡ read a\.md +$/);
|
|
1569
|
+
assert.match(running[2]!, /^ ▎ +$/);
|
|
1570
|
+
assert.match(running[3]!, /^ ▎ running… +$/);
|
|
1571
|
+
assert.match(running[4]!, /^ ▎ +$/);
|
|
1572
|
+
assert.doesNotMatch(running.join("\n"), /[╭╮╰╯│─]/);
|
|
1573
|
+
const empty = withFloatCards(() => read.renderResult(textResult(""), { expanded: false, isPartial: false }, floatToolTheme, routineRenderContext({ isPartial: false })).render(40)).map(stripAnsi);
|
|
1574
|
+
assert.deepEqual(empty, [` ▎${" ".repeat(37)} `], "an empty result only closes the panel");
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1577
|
+
test("quiet cards stay outlined in the neon style with a background-capable theme", () => {
|
|
1578
|
+
const tools = registeredQuietTools();
|
|
1579
|
+
const read = tools.get("read");
|
|
1580
|
+
const context = routineRenderContext({ args: { path: "a.md" }, isPartial: false, executionStarted: true });
|
|
1581
|
+
const rows = [
|
|
1582
|
+
...read.renderCall({ path: "a.md" }, floatToolTheme, context).render(40),
|
|
1583
|
+
...read.renderResult(textResult("one"), { expanded: false, isPartial: false }, floatToolTheme, context).render(40),
|
|
1584
|
+
].map(stripAnsi);
|
|
1585
|
+
assert.match(rows[0]!, /^╭─ ≡ read a\.md/);
|
|
1586
|
+
assert.match(rows.at(-1)!, /^╰─+╯$/);
|
|
1587
|
+
assert.doesNotMatch(rows.join("\n"), /▎/);
|
|
1588
|
+
});
|