gentle-pi 4.2.0 → 4.4.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 +340 -138
- package/assets/agents/gentle-ai-explore.md +1 -0
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -1
- package/assets/design-canvas/canvas.css +648 -0
- package/assets/design-canvas/canvas.js +1497 -0
- package/assets/design-canvas/index.html +88 -0
- package/assets/design-canvas/inspector.js +290 -0
- package/assets/orchestrator-delegation-reference.md +163 -0
- package/assets/orchestrator-delegation.md +25 -176
- package/assets/orchestrator-tracking.md +1 -1
- package/assets/orchestrator-verification.md +14 -8
- package/assets/orchestrator-writer.md +5 -1
- package/assets/orchestrator.md +11 -11
- package/bin/engram-guard +1 -1
- package/bin/gentle-shell.mjs +237 -139
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
- package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/assets/diagrams/agent-orchestration.svg +54 -15
- package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
- package/docs/assets/diagrams/measured-routing.svg +49 -0
- package/docs/assets/diagrams/native-review.svg +52 -12
- package/docs/assets/diagrams/odd-workflow.svg +128 -66
- package/docs/assets/diagrams/permission-modes.svg +51 -0
- package/docs/assets/features/audio-notifications.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/bundled-install.md +65 -30
- package/docs/delegated-verification.md +16 -8
- package/docs/gentle-agents-activity.md +16 -6
- package/docs/gentle-music.md +61 -0
- package/docs/gentle-shell-uninstall.md +8 -1
- package/docs/gentle-shell.md +80 -9
- package/docs/install-wizard.md +37 -6
- package/docs/permission-modes.md +8 -3
- package/docs/readme-reference.md +120 -28
- package/docs/session-profile-format.md +26 -0
- package/docs/voice.md +259 -0
- package/docs/yolo-mode.md +38 -10
- package/extensions/ask-user-choice.ts +4 -1
- package/extensions/codegraph-tools.ts +68 -6
- package/extensions/gentle-agents.ts +270 -48
- package/extensions/gentle-ai.ts +490 -113
- package/extensions/gentle-design.ts +158 -0
- package/extensions/gentle-herdr-activity.ts +2 -3
- package/extensions/gentle-music.ts +289 -0
- package/extensions/gentle-shell.ts +359 -85
- package/extensions/gentle-todo.ts +43 -9
- package/extensions/gentle-voice.ts +11 -0
- package/extensions/quiet-tools.ts +1 -1
- package/extensions/session-scratch.ts +384 -0
- package/extensions/skill-registry.ts +63 -8
- package/extensions/startup-banner.ts +2 -3
- package/lib/agent-home.ts +8 -3
- package/lib/agent-profiles.ts +26 -0
- package/lib/agents-config.ts +35 -0
- package/lib/agents-protocol.ts +60 -1
- package/lib/agents-runner.ts +107 -9
- package/lib/agents-session-transport.ts +101 -6
- package/lib/agents-view.ts +3 -1
- package/lib/agents-widget.ts +14 -1
- package/lib/auto-permission-memory.ts +48 -18
- package/lib/auto-permission-runtime.ts +21 -6
- package/lib/bounded-writer-admission.ts +51 -0
- package/lib/clipboard-image.ts +147 -0
- package/lib/command-palette-catalog.ts +2 -4
- package/lib/commit-slice-signal.ts +100 -0
- package/lib/context-backstop.ts +120 -0
- package/lib/delivered-test-lint.ts +80 -0
- package/lib/design-canvas-server.ts +947 -0
- package/lib/design-comments.ts +437 -0
- package/lib/design-handoffs.ts +313 -0
- package/lib/design-open-browser.ts +58 -0
- package/lib/design-sources.ts +120 -0
- package/lib/destructive-command-guard.ts +120 -9
- package/lib/external-editor.ts +31 -0
- package/lib/gauge-theme.ts +146 -0
- package/lib/gentle-ai-binary.ts +28 -7
- package/lib/gentle-design.ts +513 -0
- package/lib/gentle-shell-bundled.ts +146 -36
- package/lib/gentle-shell-import-pi.ts +233 -0
- package/lib/gentle-shell-launcher.ts +154 -15
- package/lib/gentle-shell-startup-update.ts +61 -9
- package/lib/gentle-shell-uninstall.ts +51 -2
- package/lib/herdr-activity.ts +24 -0
- package/lib/image-attachments.ts +150 -0
- package/lib/inprocess-reviewer.ts +33 -2
- package/lib/nan-provider.ts +3 -0
- package/lib/native-review-cli.ts +20 -5
- package/lib/notification-audio-native.ts +149 -13
- package/lib/notification-audio.ts +4 -0
- package/lib/notification-pulse-worker.ts +82 -18
- package/lib/notification-service.ts +5 -1
- package/lib/orchestrator-discovery.ts +5 -1
- package/lib/orchestrator-presence.ts +87 -8
- package/lib/orchestrator-scope.ts +31 -3
- package/lib/orchestrator-state.ts +11 -3
- package/lib/permission-mode-policy.ts +2 -2
- package/lib/provider-contract-bundle.ts +99 -53
- package/lib/review-candidate-view-owner.ts +29 -19
- package/lib/review-candidate-view.ts +36 -2
- package/lib/review-host-relay.ts +11 -2
- package/lib/review-reminder-receipt.ts +24 -1
- package/lib/review-risk-assessment.ts +43 -38
- package/lib/review-session-standing-permission.ts +3 -1
- package/lib/session-profile-append-controller.ts +713 -0
- package/lib/session-profile-disk-reader.ts +182 -0
- package/lib/session-profile-freeze.ts +30 -1
- package/lib/session-profile-persistence.ts +15 -7
- package/lib/session-scratch.ts +523 -0
- package/lib/session-worktree-registry.ts +7 -0
- package/lib/shell-bar.ts +29 -28
- package/lib/shell-changes-view.ts +80 -13
- package/lib/shell-changes.ts +81 -5
- package/lib/shell-gauge.ts +49 -6
- package/lib/shell-prompt.ts +4 -4
- package/lib/shell-sidebar-layout.ts +26 -6
- package/lib/shell-todo.ts +24 -5
- package/lib/shell-usage-view.ts +5 -1
- package/lib/shell-usage.ts +7 -7
- package/lib/verifier-admission.ts +30 -0
- package/lib/vim-editor-adapter.ts +11 -8
- package/lib/voice-artifacts.ts +188 -0
- package/lib/voice-command.ts +576 -0
- package/lib/voice-install.ts +455 -0
- package/lib/voice-platform.ts +99 -0
- package/lib/voice-push-to-talk.ts +262 -0
- package/lib/voice-session.ts +460 -0
- package/lib/voice-settings.ts +50 -0
- package/lib/voice-ui.ts +158 -0
- package/lib/yolo-session-policy.ts +58 -22
- package/lib/yolo-temp-scratch.ts +69 -0
- package/package.json +1 -2
- package/runtime/gentle-ai-binary.mjs +28 -7
- package/runtime/gentle-shell-bundled.mjs +145 -35
- package/runtime/gentle-shell-import-pi.mjs +234 -0
- package/runtime/gentle-shell-launcher.mjs +152 -13
- package/runtime/gentle-shell-startup-update.mjs +61 -9
- package/runtime/gentle-shell-uninstall.mjs +51 -2
- package/runtime/native-review-cli.mjs +19 -4
- package/runtime/review-risk-assessment.mjs +43 -38
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/bundled-engram-migration.mjs +119 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/installer-downloads.mjs +7 -7
- package/scripts/installer-preflight.mjs +5 -3
- package/scripts/installer-server.mjs +14 -4
- package/scripts/installer-windows.mjs +37 -7
- package/scripts/main-channel.mjs +94 -19
- package/scripts/pi-update.mjs +34 -6
- package/scripts/run-test-suite.mjs +5 -0
- package/scripts/verify-package-files.mjs +43 -13
- package/scripts/voice-worker.cjs +476 -0
- package/skills/design/SKILL.md +110 -0
- package/skills/gentle-ai/SKILL.md +3 -3
- package/skills/work-unit-commits/SKILL.md +5 -3
- package/tests/agent-profiles.test.ts +24 -0
- package/tests/agent-risk-escalation.test.ts +37 -10
- package/tests/agents-config.test.ts +61 -0
- package/tests/agents-fake-child.ts +8 -1
- package/tests/agents-protocol.test.ts +32 -0
- package/tests/agents-runner.test.ts +485 -7
- package/tests/agents-session-transport.test.ts +53 -3
- package/tests/ask-user-choice.test.ts +30 -0
- package/tests/auto-mode.test.ts +17 -0
- package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
- package/tests/auto-permission-runtime.test.ts +85 -1
- package/tests/autonomous-guard.test.ts +142 -0
- package/tests/bounded-writer-admission.test.ts +11 -3
- package/tests/bundled-install.test.ts +3 -3
- package/tests/card-content-policy.test.ts +19 -9
- package/tests/child-safety.test.ts +39 -0
- package/tests/clipboard-image.test.ts +114 -0
- package/tests/codegraph-tools.test.ts +123 -2
- package/tests/command-palette.test.ts +15 -5
- package/tests/commit-slice-signal.test.ts +162 -0
- package/tests/context-backstop.test.ts +225 -0
- package/tests/delegate-for-reason-contract.test.ts +92 -24
- package/tests/delivered-test-lint.test.ts +105 -0
- package/tests/design-canvas-assets.test.ts +710 -0
- package/tests/design-canvas-server.test.ts +1096 -0
- package/tests/design-comments.test.ts +201 -0
- package/tests/design-handoffs.test.ts +282 -0
- package/tests/design-open-browser.test.ts +70 -0
- package/tests/design-sources.test.ts +132 -0
- package/tests/destructive-command-guard.test.ts +85 -1
- package/tests/engram-safety.test.ts +132 -57
- package/tests/external-editor.test.ts +84 -0
- package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
- package/tests/gauge-theme.test.ts +120 -0
- package/tests/gentle-agents.test.ts +645 -21
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-child-guards.test.ts +61 -0
- package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
- package/tests/gentle-ai-installer.test.ts +51 -51
- package/tests/gentle-ai.test.ts +292 -13
- package/tests/gentle-design.test.ts +1313 -0
- package/tests/gentle-music.test.ts +335 -0
- package/tests/gentle-shell-bin.test.ts +163 -0
- package/tests/gentle-shell-bundled.test.ts +392 -176
- package/tests/gentle-shell-import-pi.test.ts +262 -0
- package/tests/gentle-shell-launcher.test.ts +107 -2
- package/tests/gentle-shell-startup-update.test.ts +77 -1
- package/tests/gentle-shell-uninstall.test.ts +89 -0
- package/tests/gentle-shell.test.ts +225 -17
- package/tests/gentle-todo.test.ts +190 -0
- package/tests/headless-delivery-question.test.ts +58 -0
- package/tests/herdr-activity.test.ts +34 -1
- package/tests/image-attachments.test.ts +119 -0
- package/tests/image-paste-chips.test.ts +236 -0
- package/tests/inprocess-reviewer.test.ts +55 -2
- package/tests/installer-bundles.test.ts +12 -6
- package/tests/installer-posix-bootstrap.test.ts +8 -8
- package/tests/installer-preflight.test.ts +16 -0
- package/tests/installer-server.test.ts +10 -2
- package/tests/installer-windows-bootstrap.test.ts +94 -7
- package/tests/installer-windows-diagnostics.test.ts +111 -0
- package/tests/main-channel-upgrade.test.ts +232 -2
- package/tests/main-channel.test.ts +140 -21
- package/tests/nan-provider.test.ts +36 -0
- package/tests/native-review-capability-contract.test.ts +17 -4
- package/tests/native-review-parity.test.ts +23 -2
- package/tests/no-self-pause-contract.test.ts +5 -2
- package/tests/notification-audio-native.test.ts +128 -9
- package/tests/notification-customize.test.ts +17 -5
- package/tests/odd-integration.test.ts +5 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +128 -162
- package/tests/odd-routing-contract.test.ts +115 -30
- package/tests/orchestrator-budget.test.ts +22 -8
- package/tests/orchestrator-discovery.test.ts +29 -0
- package/tests/orchestrator-presence.test.ts +100 -2
- package/tests/orchestrator-rdd-ownership.test.ts +3 -1
- package/tests/orchestrator-scope.test.ts +80 -0
- package/tests/orchestrator-state.test.ts +29 -0
- package/tests/orchestrator-status-publication-contract.test.ts +12 -8
- package/tests/package-files.test.ts +34 -0
- package/tests/package-manifest.test.ts +24 -20
- package/tests/permission-mode-policy.test.ts +10 -1
- package/tests/pi-update-launcher.test.ts +21 -2
- package/tests/pi-update.test.ts +99 -4
- package/tests/primary-session-prompt.test.ts +101 -0
- package/tests/provider-contract-bundle.test.ts +118 -0
- package/tests/provider-contract-mirror.test.ts +24 -0
- package/tests/quiet-tool-rendering.test.ts +23 -0
- package/tests/rdd-aware-verification-contract.test.ts +29 -11
- package/tests/rdd-status-line.test.ts +79 -0
- package/tests/review-agent-end-preflight.test.ts +6 -0
- package/tests/review-candidate-view.test.ts +49 -0
- package/tests/review-contract-prompt.test.ts +5 -2
- package/tests/review-controller-native-routing.test.ts +14 -1
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-routing.test.ts +147 -3
- package/tests/review-host-relay.test.ts +9 -2
- package/tests/review-json-arguments.test.ts +20 -3
- package/tests/review-loadout-rdd-off-sdk.test.ts +258 -0
- package/tests/review-owner-identity.test.ts +132 -0
- package/tests/review-relay-transport-agent.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +31 -1
- package/tests/review-risk-assessment.test.ts +163 -76
- package/tests/review-session-standing-permission-controller.test.ts +14 -1
- package/tests/review-session-standing-permission.test.ts +27 -1
- package/tests/run-test-suite.test.ts +15 -0
- package/tests/runtime-harness.mjs +16 -6
- package/tests/session-changes-shell.test.ts +1 -1
- package/tests/session-changes.test.ts +2 -1
- package/tests/session-profile-append-controller-fork.test.ts +458 -0
- package/tests/session-profile-append-controller-pi.test.ts +290 -0
- package/tests/session-profile-append-controller.test.ts +731 -0
- package/tests/session-profile-disk-reader-pi.test.ts +183 -0
- package/tests/session-profile-disk-reader.test.ts +311 -0
- package/tests/session-profile-freeze.test.ts +23 -0
- package/tests/session-scratch.test.ts +1011 -0
- package/tests/session-subject-routing-contract.test.ts +2 -1
- package/tests/session-worktree-registry.test.ts +13 -1
- package/tests/shell-bar.test.ts +21 -2
- package/tests/shell-changes-view.test.ts +137 -1
- package/tests/shell-changes.test.ts +148 -1
- package/tests/shell-gauge.test.ts +42 -0
- package/tests/shell-prompt.test.ts +3 -3
- package/tests/shell-render-cache.test.ts +134 -1
- package/tests/shell-sidebar-layout.test.ts +50 -0
- package/tests/short-check-routing-contract.test.ts +1 -1
- package/tests/skill-collision-prefixes.test.ts +3 -1
- package/tests/skill-registry-watcher.test.ts +17 -5
- package/tests/skill-registry.test.ts +47 -3
- package/tests/small-task-inline-contract.test.ts +37 -0
- package/tests/standalone-config-home.test.ts +324 -0
- package/tests/startup-banner.test.ts +5 -4
- package/tests/startup-home-repo.test.ts +34 -0
- package/tests/support/orchestrator-modules.ts +1 -0
- package/tests/task-size-routing-contract.test.ts +70 -10
- package/tests/test-scratch-contract.test.ts +102 -0
- package/tests/test-strength-contract.test.ts +36 -1
- package/tests/tool-batching-contract.test.ts +64 -0
- package/tests/verification-proportionality.test.ts +46 -0
- package/tests/verifier-admission.test.ts +154 -0
- package/tests/verify-agent-contract.test.ts +8 -8
- package/tests/voice-command.test.ts +773 -0
- package/tests/voice-install.test.ts +323 -0
- package/tests/voice-platform.test.ts +176 -0
- package/tests/voice-push-to-talk.test.ts +444 -0
- package/tests/voice-session.test.ts +434 -0
- package/tests/voice-settings.test.ts +49 -0
- package/tests/voice-ui.test.ts +97 -0
- package/tests/voice-worker.test.ts +445 -0
- package/tests/weak-model-economy-prompt.test.ts +32 -0
- package/tests/windows-hidden-processes.test.ts +30 -2
- package/tests/writer-edit-surface-scope.test.ts +3 -0
- package/tests/writer-feature-document-admission.test.ts +156 -0
- package/tests/yolo-customize.test.ts +5 -1
- package/tests/yolo-mode.test.ts +19 -1
- package/tests/yolo-session-policy-capture.test.ts +104 -0
- package/tests/yolo-temp-scratch.test.ts +71 -0
- package/scripts/mirror-odd-routing.mjs +0 -242
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { designCommentBatchContent, designCommentContent, hasSystemTokens, listDesignEntries, readDesignManifest, resolveSystemBoard, startDesignCanvasServer, type DesignArtboard, type DesignCanvasServer, type DesignCanvasServerOptions, type DesignSessionContent } from "./design-canvas-server.ts";
|
|
3
|
+
import { createDesignCommentStore, DesignCommentsError, type StoredDesignComment } from "./design-comments.ts";
|
|
4
|
+
import { createDesignHandoffStore, DesignHandoffsError, sendDesignHandoff, type DesignHandoff, type DesignHandoffDelivery, type DesignHandoffStatus } from "./design-handoffs.ts";
|
|
5
|
+
import { openInBrowser, type OpenInBrowserResult } from "./design-open-browser.ts";
|
|
6
|
+
import { detectDesignSources, staleDesignSources } from "./design-sources.ts";
|
|
7
|
+
|
|
8
|
+
// Gentle Design helpers: argument parsing, the designs already written under
|
|
9
|
+
// design/<slug>/design.json, the usage text, the per-session canvas that
|
|
10
|
+
// open/stop drive, and the comments tool. The extension stays a thin wrapper.
|
|
11
|
+
|
|
12
|
+
export const DESIGN_SKILL_NAME = "gentle-design";
|
|
13
|
+
|
|
14
|
+
export const DESIGN_SUBCOMMANDS = ["open", "stop", "sync", "implement"] as const;
|
|
15
|
+
export type DesignSubcommand = (typeof DESIGN_SUBCOMMANDS)[number];
|
|
16
|
+
|
|
17
|
+
export type DesignArgs =
|
|
18
|
+
| { kind: "usage" }
|
|
19
|
+
| { kind: "subcommand"; name: DesignSubcommand; rest: string }
|
|
20
|
+
| { kind: "brief"; brief: string };
|
|
21
|
+
|
|
22
|
+
export function parseDesignArgs(args: string): DesignArgs {
|
|
23
|
+
const text = args.trim();
|
|
24
|
+
if (!text) return { kind: "usage" };
|
|
25
|
+
const [head = "", ...rest] = text.split(/\s+/);
|
|
26
|
+
const name = DESIGN_SUBCOMMANDS.find((subcommand) => subcommand === head);
|
|
27
|
+
return name ? { kind: "subcommand", name, rest: rest.join(" ") } : { kind: "brief", brief: text };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DesignSummary {
|
|
31
|
+
slug: string;
|
|
32
|
+
title: string;
|
|
33
|
+
/** design.json modification time in milliseconds. */
|
|
34
|
+
mtime: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Designs under <cwd>/design, through the listing the canvas server uses (same containment rule). */
|
|
38
|
+
export function listDesigns(cwd: string): DesignSummary[] {
|
|
39
|
+
return listDesignEntries(join(cwd, "design")).map(({ slug, title, mtime }) => ({ slug, title, mtime }));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Whether design/system.html exists, so the canvas can open on the system board alone. */
|
|
43
|
+
export function hasSystemBoard(cwd: string): boolean {
|
|
44
|
+
return resolveSystemBoard(join(cwd, "design")) !== undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const DESIGN_HELP =
|
|
48
|
+
"Design HTML artboards with any model: /gentle:design <brief> starts a design run; no arguments open the latest design (or the system board alone) on the local canvas. Subcommands: open [slug], stop, sync (write design/system.md and design/system.html from the project's design sources; the canvas shows that system board first), implement <slug>/<artboard> (hand an artboard off to the project's real stack, also from the canvas Implement button).";
|
|
49
|
+
|
|
50
|
+
/** What /gentle:design sync asks the skill to do; it rides designRunMessage like a brief. */
|
|
51
|
+
export const DESIGN_SYNC_REQUEST =
|
|
52
|
+
'sync the project design system: follow "Design system sync" to read the project\'s design sources and write design/system.md and design/system.html.';
|
|
53
|
+
|
|
54
|
+
export function designUsage(designs: readonly DesignSummary[]): string {
|
|
55
|
+
const lines = ["Usage: /gentle:design <brief>", "Describe the goal, layout, content, and audience. Directions are written to design/<slug>/."];
|
|
56
|
+
if (designs.length > 0) {
|
|
57
|
+
lines.push("", "Designs in this project:");
|
|
58
|
+
for (const design of designs) lines.push(` ${design.title} (${design.slug})`);
|
|
59
|
+
}
|
|
60
|
+
return lines.join("\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The message that runs the skill: a skill command when Pi has it loaded, otherwise a pointer to the bundled file. */
|
|
64
|
+
export function designRunMessage(brief: string, skill: { loaded: boolean; path: string }): string {
|
|
65
|
+
if (skill.loaded) return `/skill:${DESIGN_SKILL_NAME} ${brief}`;
|
|
66
|
+
return `Read the ${DESIGN_SKILL_NAME} skill at ${skill.path} and follow it for this design brief:\n\n${brief}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type DesignNotice = { message: string; level: "info" | "warning" | "error" };
|
|
70
|
+
|
|
71
|
+
export type DesignImplementInput = {
|
|
72
|
+
slug: string;
|
|
73
|
+
artboard: Pick<DesignArtboard, "file" | "title" | "tradeoff">;
|
|
74
|
+
/** The design's valid ODD feature document, when design.json links one. */
|
|
75
|
+
feature?: string;
|
|
76
|
+
/** Whether design/system.md exists. */
|
|
77
|
+
systemTokens: boolean;
|
|
78
|
+
/** The open comments on this artboard. */
|
|
79
|
+
comments: ReadonlyArray<Pick<StoredDesignComment, "id" | "selector" | "excerpt" | "comment">>;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** The handoff message for one artboard (S8); the command and the canvas button both send exactly this. */
|
|
83
|
+
export function designImplementMessage(input: DesignImplementInput): string {
|
|
84
|
+
const path = `design/${input.slug}/${input.artboard.file}`;
|
|
85
|
+
const lines = [
|
|
86
|
+
`Implement the design artboard ${path}`,
|
|
87
|
+
"",
|
|
88
|
+
`Artboard: ${path}`,
|
|
89
|
+
`Title: ${input.artboard.title}`,
|
|
90
|
+
];
|
|
91
|
+
if (input.artboard.tradeoff) lines.push(`Tradeoff: ${input.artboard.tradeoff}`);
|
|
92
|
+
lines.push(
|
|
93
|
+
"",
|
|
94
|
+
"Implement it in the project's real stack. Use the artboard as the visual spec: map its parts to the project's existing components and styles, and do not copy the artboard HTML verbatim.",
|
|
95
|
+
);
|
|
96
|
+
if (input.systemTokens) lines.push("Take colors, typography, spacing, radius and shadows from design/system.md, the project's token source.");
|
|
97
|
+
if (input.comments.length > 0) {
|
|
98
|
+
lines.push("", `Open comments on this artboard (${input.comments.length}); apply each one in the implementation:`);
|
|
99
|
+
input.comments.forEach((comment, index) => {
|
|
100
|
+
lines.push(
|
|
101
|
+
`${index + 1}. Comment id: ${comment.id}`,
|
|
102
|
+
` Element: ${comment.selector}`,
|
|
103
|
+
` Excerpt: ${comment.excerpt ? JSON.stringify(comment.excerpt) : "(none)"}`,
|
|
104
|
+
" Comment:",
|
|
105
|
+
...comment.comment.split("\n").map((line) => ` ${line}`),
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
const ids = input.comments.map((comment) => JSON.stringify(comment.id)).join(", ");
|
|
109
|
+
lines.push(`When they are applied, resolve them with the ${DESIGN_COMMENTS_TOOL_NAME} tool (action "resolve", slug "${input.slug}", ids [${ids}]).`);
|
|
110
|
+
}
|
|
111
|
+
lines.push(
|
|
112
|
+
"",
|
|
113
|
+
`Report progress with the ${DESIGN_TOOL_NAME} tool: action "handoff", target "${input.slug}/${input.artboard.file}", status "in_progress" when you start; status "implemented" when it is done, with previewUrl set to the running app's loopback URL (localhost, 127.0.0.1 or [::1]) when one is running, so the canvas can compare it with the artboard.`,
|
|
114
|
+
input.feature
|
|
115
|
+
? `This design belongs to an ODD feature: track this implementation in ${input.feature} (add or reopen its task there and log this handoff).`
|
|
116
|
+
: "This design is not linked to an ODD feature document: size it under the project's ODD rules as usual.",
|
|
117
|
+
"This handoff itself commits nothing.",
|
|
118
|
+
);
|
|
119
|
+
return lines.join("\n");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The artboard a handoff records in handoffs.json (S15). */
|
|
123
|
+
export type DesignHandoffTarget = { slug: string; file: string; feature?: string };
|
|
124
|
+
|
|
125
|
+
/** `warnings` name what design.json dropped (an invalid feature link), so the command can report it. */
|
|
126
|
+
export type DesignImplementPlan = { kind: "send"; message: string; warnings: string[]; handoff: DesignHandoffTarget } | { kind: "refuse"; notice: DesignNotice };
|
|
127
|
+
|
|
128
|
+
function implementTargets(cwd: string): string[] {
|
|
129
|
+
const targets: string[] = [];
|
|
130
|
+
for (const design of listDesigns(cwd)) {
|
|
131
|
+
try {
|
|
132
|
+
for (const artboard of readDesignManifest(join(cwd, "design"), design.slug).artboards) targets.push(`${design.slug}/${artboard.file}`);
|
|
133
|
+
} catch {
|
|
134
|
+
// An unreadable design has no artboard to hand off.
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return targets;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
type DesignRefusal = Extract<DesignImplementPlan, { kind: "refuse" }>;
|
|
141
|
+
|
|
142
|
+
function unknownTarget(cwd: string, reason: string): DesignRefusal {
|
|
143
|
+
const targets = implementTargets(cwd);
|
|
144
|
+
if (targets.length === 0) return { kind: "refuse", notice: { message: `${reason}\nNo designs in design/ yet.\n${designUsage([])}`, level: "warning" } };
|
|
145
|
+
const shown = targets.length > 30 ? [...targets.slice(0, 30), `… ${targets.length - 30} more`] : targets;
|
|
146
|
+
return { kind: "refuse", notice: { message: `${reason}\nUsage: /gentle:design implement <slug>/<artboard>. Available: ${shown.join(", ")}.`, level: "warning" } };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
type ResolvedTarget = { kind: "artboard"; slug: string; artboard: DesignArtboard; feature?: string; warnings: string[] };
|
|
150
|
+
|
|
151
|
+
/** `<slug>/<file>` checked against the design's manifest; `what` names the action in the refusal. */
|
|
152
|
+
function resolveDesignTarget(cwd: string, target: string, what: string): ResolvedTarget | DesignRefusal {
|
|
153
|
+
const text = target.trim();
|
|
154
|
+
const slash = text.indexOf("/");
|
|
155
|
+
const slug = slash === -1 ? text : text.slice(0, slash);
|
|
156
|
+
const file = slash === -1 ? "" : text.slice(slash + 1);
|
|
157
|
+
if (!slug || !file) return unknownTarget(cwd, `Name the artboard to ${what} as <slug>/<artboard>.`);
|
|
158
|
+
if (!listDesigns(cwd).some((design) => design.slug === slug)) return unknownTarget(cwd, `No design "${slug}" in design/.`);
|
|
159
|
+
let manifest;
|
|
160
|
+
try {
|
|
161
|
+
manifest = readDesignManifest(join(cwd, "design"), slug);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
return { kind: "refuse", notice: { message: error instanceof Error ? error.message : String(error), level: "warning" } };
|
|
164
|
+
}
|
|
165
|
+
const artboard = manifest.artboards.find((candidate) => candidate.file === file);
|
|
166
|
+
if (!artboard) return unknownTarget(cwd, `No artboard "${file}" in design/${slug}/design.json.`);
|
|
167
|
+
const warnings = (manifest.warnings ?? []).map((warning) => `design/${slug}/design.json: ${warning}`);
|
|
168
|
+
return { kind: "artboard", slug, artboard, feature: manifest.feature, warnings };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Validates `<slug>/<file>` against the design's manifest and builds its handoff message. */
|
|
172
|
+
export async function planDesignImplement(cwd: string, target: string): Promise<DesignImplementPlan> {
|
|
173
|
+
const resolved = resolveDesignTarget(cwd, target, "implement");
|
|
174
|
+
if (resolved.kind === "refuse") return resolved;
|
|
175
|
+
const { slug, artboard, feature, warnings } = resolved;
|
|
176
|
+
const designRoot = join(cwd, "design");
|
|
177
|
+
let comments: StoredDesignComment[];
|
|
178
|
+
try {
|
|
179
|
+
comments = await createDesignCommentStore(designRoot).listOpen(slug, artboard.file);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
const reason = error instanceof DesignCommentsError && error.code === "invalid-file"
|
|
182
|
+
? `design/${slug}/comments.json cannot be read: ${error.message}. Fix the file by hand; it is never overwritten.`
|
|
183
|
+
: error instanceof Error ? error.message : String(error);
|
|
184
|
+
return { kind: "refuse", notice: { message: reason, level: "warning" } };
|
|
185
|
+
}
|
|
186
|
+
return { kind: "send", message: designImplementMessage({ slug, artboard, feature, systemTokens: hasSystemTokens(designRoot), comments }), warnings, handoff: { slug, file: artboard.file, feature } };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** A handoffs.json problem as the reason the user or the model reads; a broken file is never overwritten. */
|
|
190
|
+
function handoffsProblem(slug: string, error: unknown): Error {
|
|
191
|
+
if (error instanceof DesignHandoffsError && error.code === "invalid-file") {
|
|
192
|
+
return new Error(`design/${slug}/handoffs.json cannot be read: ${error.message}. Fix the file by hand; it is never overwritten.`);
|
|
193
|
+
}
|
|
194
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Sends a planned handoff (the command): recorded in handoffs.json as sent and
|
|
199
|
+
* chosen before `deliver` runs, then marked queued when the delivery says so.
|
|
200
|
+
*/
|
|
201
|
+
export async function sendDesignImplement(cwd: string, plan: Extract<DesignImplementPlan, { kind: "send" }>, deliver: (message: string) => DesignHandoffDelivery | Promise<DesignHandoffDelivery>): Promise<{ queued: boolean }> {
|
|
202
|
+
const store = createDesignHandoffStore(join(cwd, "design"));
|
|
203
|
+
return sendDesignHandoff(store, plan.handoff, () => deliver(plan.message)).catch((error: unknown) => {
|
|
204
|
+
throw error instanceof DesignHandoffsError ? handoffsProblem(plan.handoff.slug, error) : error;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface DesignCanvasDeps {
|
|
209
|
+
startServer?: (options: DesignCanvasServerOptions) => Promise<DesignCanvasServer>;
|
|
210
|
+
openBrowser?: (url: string) => Promise<OpenInBrowserResult>;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* `url` is set once the canvas shows something; `opened` says whether a browser tab shows it.
|
|
215
|
+
* `refreshed` is true when an already connected canvas page was told to show and reload it,
|
|
216
|
+
* so no new tab was opened (S14).
|
|
217
|
+
*/
|
|
218
|
+
export type DesignOpenNotice = DesignNotice & { url?: string; opened?: boolean; refreshed?: boolean };
|
|
219
|
+
|
|
220
|
+
export interface DesignCanvas {
|
|
221
|
+
/**
|
|
222
|
+
* Shows `slug`, or the most recent design, on the session's canvas server, starting it when needed.
|
|
223
|
+
* A running server with a connected canvas page refreshes that page instead of opening the browser.
|
|
224
|
+
*/
|
|
225
|
+
open(cwd: string, slug?: string): Promise<DesignOpenNotice>;
|
|
226
|
+
/** Closes the running server; false when none was running. */
|
|
227
|
+
stop(): Promise<boolean>;
|
|
228
|
+
/** The URL the running canvas last showed; undefined when none is running. */
|
|
229
|
+
url(): string | undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** The canvas of `slug`, or the root canvas (system board only) when slug is null. */
|
|
233
|
+
function canvasUrl(server: DesignCanvasServer, slug: string | null): string {
|
|
234
|
+
return new URL(slug === null ? `/${server.token}/` : `/${server.token}/d/${slug}/`, server.url).href;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* One canvas server per session; comment messages go to `deliver`, whose failure the server reports as HTTP 500.
|
|
239
|
+
* For a handoff, `deliver` also answers whether the message was queued behind a busy agent.
|
|
240
|
+
* A comment with a readable screenshot arrives as text followed by the attached image.
|
|
241
|
+
*/
|
|
242
|
+
export function createDesignCanvas(deliver: (message: DesignSessionContent) => DesignHandoffDelivery | Promise<DesignHandoffDelivery>, deps: DesignCanvasDeps = {}): DesignCanvas {
|
|
243
|
+
const startServer = deps.startServer ?? startDesignCanvasServer;
|
|
244
|
+
const openBrowser = deps.openBrowser ?? ((url: string) => openInBrowser(url));
|
|
245
|
+
let running: { root: string; server: Promise<DesignCanvasServer>; url?: string } | undefined;
|
|
246
|
+
|
|
247
|
+
async function stop(): Promise<boolean> {
|
|
248
|
+
const current = running;
|
|
249
|
+
running = undefined;
|
|
250
|
+
if (!current) return false;
|
|
251
|
+
const server = await current.server.catch(() => undefined);
|
|
252
|
+
await server?.close();
|
|
253
|
+
return server !== undefined;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function serverFor(cwd: string): Promise<DesignCanvasServer> {
|
|
257
|
+
const root = join(cwd, "design");
|
|
258
|
+
// A new session cwd means a different design root, so the old server goes.
|
|
259
|
+
if (running && running.root !== root) await stop();
|
|
260
|
+
if (!running) {
|
|
261
|
+
const server = startServer({
|
|
262
|
+
designRoot: root,
|
|
263
|
+
onComment: async (comment) => { await deliver(designCommentContent(comment)); },
|
|
264
|
+
onComments: async (batch) => { await deliver(designCommentBatchContent(batch)); },
|
|
265
|
+
// The same plan as /gentle:design implement, so both send identical text; the server
|
|
266
|
+
// has already recorded the handoff, and this answers whether it was queued.
|
|
267
|
+
onImplement: async ({ slug, file }) => {
|
|
268
|
+
const plan = await planDesignImplement(cwd, `${slug}/${file}`);
|
|
269
|
+
if (plan.kind === "refuse") throw new Error(plan.notice.message);
|
|
270
|
+
return deliver(plan.message);
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
running = { root, server };
|
|
274
|
+
server.catch(() => { if (running?.server === server) running = undefined; });
|
|
275
|
+
}
|
|
276
|
+
return running.server;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function open(cwd: string, requested?: string): Promise<DesignOpenNotice> {
|
|
280
|
+
const designs = listDesigns(cwd);
|
|
281
|
+
// With only design/system.html, the canvas opens on the system board alone (L14).
|
|
282
|
+
const systemOnly = designs.length === 0 && requested === undefined && hasSystemBoard(cwd);
|
|
283
|
+
if (designs.length === 0 && !systemOnly) {
|
|
284
|
+
return { message: `No designs in design/ yet.\n${designUsage([])}`, level: "warning" };
|
|
285
|
+
}
|
|
286
|
+
const slug = systemOnly ? null : requested ?? designs[0].slug;
|
|
287
|
+
if (slug !== null && !designs.some((design) => design.slug === slug)) {
|
|
288
|
+
return { message: `No design "${slug}" in design/. Available: ${designs.map((design) => design.slug).join(", ")}.`, level: "warning" };
|
|
289
|
+
}
|
|
290
|
+
// Only a server that was already running can have a canvas page of its own connected.
|
|
291
|
+
const reused = running?.root === join(cwd, "design");
|
|
292
|
+
let server: DesignCanvasServer;
|
|
293
|
+
try {
|
|
294
|
+
server = await serverFor(cwd);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
return { message: `The design canvas could not start: ${error instanceof Error ? error.message : String(error)}`, level: "error" };
|
|
297
|
+
}
|
|
298
|
+
const url = canvasUrl(server, slug);
|
|
299
|
+
if (running) running.url = url;
|
|
300
|
+
if (reused && server.connectedClients() > 0 && server.show(slug) > 0) {
|
|
301
|
+
return { message: `Design canvas refreshed: ${url}`, level: "info", url, opened: true, refreshed: true };
|
|
302
|
+
}
|
|
303
|
+
const result = await openBrowser(url);
|
|
304
|
+
if (result.opened) return { message: `Design canvas: ${url}`, level: "info", url, opened: true };
|
|
305
|
+
const reason = result.error ? ` (${result.error})` : "";
|
|
306
|
+
return { message: `Design canvas: ${url}\nThe browser could not be opened${reason}. Open the URL manually.`, level: "warning", url, opened: false };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return { open, stop, url: () => running?.url };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export const DESIGN_COMMENTS_TOOL_NAME = "gentle_design_comments";
|
|
313
|
+
|
|
314
|
+
export type DesignCommentsToolParams = { action: "list" | "resolve"; slug?: string; file?: string; ids?: string[] };
|
|
315
|
+
export type DesignCommentsToolResult = { text: string; details: { comments: Array<StoredDesignComment & { slug: string }> } | { slug: string; resolved: string[]; unknown: string[] } };
|
|
316
|
+
|
|
317
|
+
function describeComment(slug: string, comment: StoredDesignComment): string[] {
|
|
318
|
+
return [
|
|
319
|
+
`- [${comment.id}] design/${slug}/${comment.file}`,
|
|
320
|
+
` Element: ${comment.selector}`,
|
|
321
|
+
` Excerpt: ${comment.excerpt ? JSON.stringify(comment.excerpt) : "(none)"}`,
|
|
322
|
+
` Comment: ${comment.comment.split("\n").join("\n ")}`,
|
|
323
|
+
...(comment.screenshot ? [` Screenshot: design/${slug}/${comment.screenshot} (open it with the read tool)`] : []),
|
|
324
|
+
];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** list: open comments (all designs, or one design and artboard); resolve: mark ids of one design resolved. */
|
|
328
|
+
export async function runDesignCommentsTool(cwd: string, params: DesignCommentsToolParams): Promise<DesignCommentsToolResult> {
|
|
329
|
+
const store = createDesignCommentStore(join(cwd, "design"));
|
|
330
|
+
const failure = (slug: string, error: unknown) => error instanceof DesignCommentsError && error.code === "invalid-file"
|
|
331
|
+
? new Error(`design/${slug}/comments.json cannot be read: ${error.message}. Fix the file by hand; it is never overwritten.`)
|
|
332
|
+
: error;
|
|
333
|
+
if (params.action === "resolve") {
|
|
334
|
+
if (typeof params.slug !== "string" || !params.slug) throw new Error("resolve needs the design slug.");
|
|
335
|
+
const ids = params.ids ?? [];
|
|
336
|
+
if (ids.length === 0) throw new Error("resolve needs at least one comment id in ids.");
|
|
337
|
+
const { updated, unknown } = await store.setStatus(params.slug, ids, "resolved").catch((error: unknown) => { throw failure(params.slug!, error); });
|
|
338
|
+
const resolved = updated.map((comment) => comment.id);
|
|
339
|
+
const lines = [`Resolved ${resolved.length} comment${resolved.length === 1 ? "" : "s"} in design/${params.slug}${resolved.length ? `: ${resolved.join(", ")}` : ""}.`];
|
|
340
|
+
if (unknown.length) lines.push(`Unknown ids: ${unknown.join(", ")}.`);
|
|
341
|
+
return { text: lines.join("\n"), details: { slug: params.slug, resolved, unknown } };
|
|
342
|
+
}
|
|
343
|
+
const slugs = params.slug ? [params.slug] : listDesigns(cwd).map((design) => design.slug);
|
|
344
|
+
const found: Array<StoredDesignComment & { slug: string }> = [];
|
|
345
|
+
const problems: string[] = [];
|
|
346
|
+
for (const slug of slugs) {
|
|
347
|
+
try {
|
|
348
|
+
for (const comment of await store.listOpen(slug, params.file)) found.push({ slug, ...comment });
|
|
349
|
+
} catch (error) {
|
|
350
|
+
// One design's broken file must not hide the others, unless it was asked for by name.
|
|
351
|
+
if (params.slug) throw failure(slug, error);
|
|
352
|
+
problems.push(`design/${slug}/comments.json: ${error instanceof Error ? error.message : String(error)}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const scope = params.slug ? `design/${params.slug}${params.file ? `/${params.file}` : ""}` : "design/";
|
|
356
|
+
const lines = found.length === 0
|
|
357
|
+
? [`No open comments in ${scope}.`]
|
|
358
|
+
: [`Open design comments in ${scope} (${found.length}):`, ...found.flatMap((comment) => describeComment(comment.slug, comment)), "", `After applying a comment, resolve it with ${DESIGN_COMMENTS_TOOL_NAME} (action "resolve", slug, ids).`];
|
|
359
|
+
if (problems.length) lines.push("", "Unreadable comment files:", ...problems.map((problem) => `- ${problem}`));
|
|
360
|
+
return { text: lines.join("\n"), details: { comments: found } };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export const DESIGN_TOOL_NAME = "gentle_design";
|
|
364
|
+
|
|
365
|
+
/** Appended to the system prompt while the gentle-design skill is available (S11). */
|
|
366
|
+
export const DESIGN_PROMPT_HINT = [
|
|
367
|
+
`Gentle Design: any request for a UI design, mockup, screen, landing page or visual directions uses the ${DESIGN_SKILL_NAME} skill by default; read the skill first when it is not loaded.`,
|
|
368
|
+
`Before designing, call the ${DESIGN_TOOL_NAME} tool (action "list"); when the project has design sources but no design/system.md, run the skill's design system sync first.`,
|
|
369
|
+
`After writing or changing a design, open or refresh it on the canvas with the ${DESIGN_TOOL_NAME} tool (action "open" with the slug), unless the user said not to.`,
|
|
370
|
+
"Designs stay under design/<slug>/.",
|
|
371
|
+
].join("\n");
|
|
372
|
+
|
|
373
|
+
export type DesignToolParams = {
|
|
374
|
+
action: "list" | "open" | "stop" | "implement" | "handoff";
|
|
375
|
+
slug?: string;
|
|
376
|
+
target?: string;
|
|
377
|
+
/** handoff only. */
|
|
378
|
+
status?: string;
|
|
379
|
+
note?: string;
|
|
380
|
+
previewUrl?: string;
|
|
381
|
+
};
|
|
382
|
+
/** How many design source paths list names; `total` still counts them all. */
|
|
383
|
+
const LISTED_DESIGN_SOURCES = 20;
|
|
384
|
+
|
|
385
|
+
export type DesignSystemState = { exists: boolean; file: "design/system.md"; staleSources: string[] };
|
|
386
|
+
export type DesignToolDetails =
|
|
387
|
+
| {
|
|
388
|
+
designs: Array<{ slug: string; title: string; artboards: Array<{ file: string; title: string }>; error?: string }>;
|
|
389
|
+
systemBoard: boolean;
|
|
390
|
+
designSources: { paths: string[]; total: number };
|
|
391
|
+
system: DesignSystemState;
|
|
392
|
+
canvas: { running: boolean; url?: string };
|
|
393
|
+
}
|
|
394
|
+
| { url: string; opened: boolean; refreshed?: true }
|
|
395
|
+
| { stopped: boolean }
|
|
396
|
+
| { target: string; warnings: string[] }
|
|
397
|
+
| { target: string; chosen: string | null; handoff: DesignHandoff };
|
|
398
|
+
/** `texts` become the tool's content parts; a refusal throws, so Pi flags the result as an error. */
|
|
399
|
+
export type DesignToolResult = { texts: string[]; details: DesignToolDetails };
|
|
400
|
+
|
|
401
|
+
function handoffState(entry: DesignHandoff): string {
|
|
402
|
+
const status = entry.status === "in_progress" ? "in progress" : entry.status;
|
|
403
|
+
const parts = [`handoff ${status}${entry.status === "sent" && entry.queued ? ", queued" : ""}`];
|
|
404
|
+
if (entry.previewUrl) parts.push(`preview ${entry.previewUrl}`);
|
|
405
|
+
return parts.join("; ");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
type ListedDesign = {
|
|
409
|
+
slug: string;
|
|
410
|
+
title: string;
|
|
411
|
+
artboards: Array<{ file: string; title: string }>;
|
|
412
|
+
error?: string;
|
|
413
|
+
/** Present only when the design has handoff records (S15). */
|
|
414
|
+
chosen?: string | null;
|
|
415
|
+
handoffs?: Record<string, DesignHandoff>;
|
|
416
|
+
handoffsError?: string;
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
async function listDesignTool(cwd: string, canvas: DesignCanvas): Promise<DesignToolResult> {
|
|
420
|
+
const designRoot = join(cwd, "design");
|
|
421
|
+
const handoffStore = createDesignHandoffStore(designRoot);
|
|
422
|
+
const designs: ListedDesign[] = [];
|
|
423
|
+
for (const { slug, title } of listDesigns(cwd)) {
|
|
424
|
+
let design: ListedDesign;
|
|
425
|
+
try {
|
|
426
|
+
const artboards = readDesignManifest(designRoot, slug).artboards.map(({ file, title }) => ({ file, title }));
|
|
427
|
+
design = { slug, title, artboards };
|
|
428
|
+
} catch (error) {
|
|
429
|
+
design = { slug, title, artboards: [], error: error instanceof Error ? error.message : String(error) };
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
const document = await handoffStore.read(slug);
|
|
433
|
+
if (document.chosen !== null || Object.keys(document.handoffs).length > 0) {
|
|
434
|
+
design.chosen = document.chosen;
|
|
435
|
+
design.handoffs = document.handoffs;
|
|
436
|
+
}
|
|
437
|
+
} catch (error) {
|
|
438
|
+
design.handoffsError = error instanceof Error ? error.message : String(error);
|
|
439
|
+
}
|
|
440
|
+
designs.push(design);
|
|
441
|
+
}
|
|
442
|
+
const systemBoard = hasSystemBoard(cwd);
|
|
443
|
+
const url = canvas.url();
|
|
444
|
+
const lines = designs.length === 0 ? ["No designs in design/ yet."] : [`Designs in design/ (${designs.length}):`];
|
|
445
|
+
for (const design of designs) {
|
|
446
|
+
lines.push(`- ${design.slug}: ${design.title}${design.error ? ` (design.json cannot be read: ${design.error})` : ""}`);
|
|
447
|
+
if (design.chosen) lines.push(` Chosen: ${design.chosen} (the other artboards are alternatives)`);
|
|
448
|
+
if (design.handoffsError) lines.push(` handoffs.json cannot be read: ${design.handoffsError}`);
|
|
449
|
+
for (const artboard of design.artboards) {
|
|
450
|
+
const entry = design.handoffs && Object.hasOwn(design.handoffs, artboard.file) ? design.handoffs[artboard.file] : undefined;
|
|
451
|
+
lines.push(` - ${artboard.file}: ${artboard.title}${entry ? ` (${handoffState(entry)})` : ""}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
lines.push(systemBoard ? "System board: design/system.html (shown first on the canvas)." : "No system board (design/system.html).");
|
|
455
|
+
const sources = detectDesignSources(cwd);
|
|
456
|
+
const exists = hasSystemTokens(designRoot);
|
|
457
|
+
const system: DesignSystemState = { exists, file: "design/system.md", staleSources: exists ? staleDesignSources(cwd, sources) : [] };
|
|
458
|
+
lines.push(sources.length === 0 ? "Design sources: none detected." : `Design sources (${sources.length}): ${capped(sources)}`);
|
|
459
|
+
if (!system.exists) lines.push(`Design system: no design/system.md.${sources.length > 0 ? " Run the skill's Design system sync before designing." : ""}`);
|
|
460
|
+
else if (system.staleSources.length === 0) lines.push("Design system: design/system.md is up to date.");
|
|
461
|
+
else {
|
|
462
|
+
const count = system.staleSources.length;
|
|
463
|
+
lines.push(`Design system: design/system.md is older than ${count} source${count === 1 ? "" : "s"} changed after it: ${capped(system.staleSources)}. Say its tokens may be outdated and offer to sync again before designing.`);
|
|
464
|
+
}
|
|
465
|
+
lines.push(url ? `Canvas: running at ${url}` : "Canvas: not running.");
|
|
466
|
+
const designSources = { paths: sources.slice(0, LISTED_DESIGN_SOURCES), total: sources.length };
|
|
467
|
+
return { texts: [lines.join("\n")], details: { designs, systemBoard, designSources, system, canvas: url ? { running: true, url } : { running: false } } };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function capped(paths: readonly string[]): string {
|
|
471
|
+
const shown = paths.slice(0, LISTED_DESIGN_SOURCES);
|
|
472
|
+
if (paths.length > shown.length) shown.push(`… ${paths.length - shown.length} more`);
|
|
473
|
+
return shown.join(", ");
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** The handoff action: the agent's progress on one artboard, validated by the manifest and the store. */
|
|
477
|
+
async function handoffTool(cwd: string, params: DesignToolParams): Promise<DesignToolResult> {
|
|
478
|
+
if (params.status !== "in_progress" && params.status !== "implemented") throw new Error('handoff needs status "in_progress" or "implemented".');
|
|
479
|
+
const status: Exclude<DesignHandoffStatus, "sent"> = params.status;
|
|
480
|
+
const resolved = resolveDesignTarget(cwd, params.target ?? "", "report on");
|
|
481
|
+
if (resolved.kind === "refuse") throw new Error(resolved.notice.message);
|
|
482
|
+
const { slug, artboard } = resolved;
|
|
483
|
+
const store = createDesignHandoffStore(join(cwd, "design"));
|
|
484
|
+
const handoff = await store.setStatus(slug, artboard.file, status, { note: params.note, previewUrl: params.previewUrl }).catch((error: unknown) => { throw handoffsProblem(slug, error); });
|
|
485
|
+
const { chosen } = await store.read(slug).catch((error: unknown) => { throw handoffsProblem(slug, error); });
|
|
486
|
+
const path = `design/${slug}/${artboard.file}`;
|
|
487
|
+
const lines = [`Handoff of ${path}: ${status === "in_progress" ? "in progress" : "implemented"}.${chosen === artboard.file ? ` It is the chosen artboard of design/${slug}.` : ""}`];
|
|
488
|
+
if (handoff.previewUrl) lines.push(`Preview: ${handoff.previewUrl}${status === "implemented" ? " (the canvas offers Compare)" : ""}.`);
|
|
489
|
+
return { texts: [lines.join("\n")], details: { target: `${slug}/${artboard.file}`, chosen, handoff } };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** The gentle_design tool: the /gentle:design actions on the session's canvas, without sending a user message. */
|
|
493
|
+
export async function runDesignTool(cwd: string, params: DesignToolParams, canvas: DesignCanvas): Promise<DesignToolResult> {
|
|
494
|
+
if (params.action === "handoff") return handoffTool(cwd, params);
|
|
495
|
+
if (params.action === "list") return listDesignTool(cwd, canvas);
|
|
496
|
+
if (params.action === "stop") {
|
|
497
|
+
const stopped = await canvas.stop();
|
|
498
|
+
return { texts: [stopped ? "Design canvas stopped." : "No design canvas is running."], details: { stopped } };
|
|
499
|
+
}
|
|
500
|
+
if (params.action === "open") {
|
|
501
|
+
const notice = await canvas.open(cwd, params.slug || undefined);
|
|
502
|
+
if (notice.url === undefined) throw new Error(notice.message);
|
|
503
|
+
const details = notice.refreshed ? { url: notice.url, opened: true, refreshed: true as const } : { url: notice.url, opened: notice.opened === true };
|
|
504
|
+
return { texts: [notice.message], details };
|
|
505
|
+
}
|
|
506
|
+
const target = params.target ?? "";
|
|
507
|
+
const plan = await planDesignImplement(cwd, target);
|
|
508
|
+
if (plan.kind === "refuse") throw new Error(plan.notice.message);
|
|
509
|
+
// The model follows this handoff in its current run, so it is never queued.
|
|
510
|
+
await createDesignHandoffStore(join(cwd, "design")).markSent(plan.handoff.slug, plan.handoff.file, { queued: false, feature: plan.handoff.feature })
|
|
511
|
+
.catch((error: unknown) => { throw handoffsProblem(plan.handoff.slug, error); });
|
|
512
|
+
return { texts: [plan.message, ...plan.warnings.map((warning) => `Warning: ${warning}`)], details: { target: target.trim(), warnings: plan.warnings } };
|
|
513
|
+
}
|