@bastani/atomic 0.9.13 → 0.9.14-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/README.md +3 -3
- package/dist/builtin/intercom/package.json +2 -2
- package/dist/builtin/mcp/package.json +3 -3
- package/dist/builtin/subagents/CHANGELOG.md +14 -0
- package/dist/builtin/subagents/README.md +6 -4
- package/dist/builtin/subagents/agents/code-simplifier.md +1 -1
- package/dist/builtin/subagents/agents/codebase-analyzer.md +1 -1
- package/dist/builtin/subagents/agents/codebase-locator.md +1 -1
- package/dist/builtin/subagents/agents/codebase-online-researcher.md +1 -1
- package/dist/builtin/subagents/agents/codebase-pattern-finder.md +1 -1
- package/dist/builtin/subagents/agents/codebase-research-analyzer.md +1 -1
- package/dist/builtin/subagents/agents/codebase-research-locator.md +1 -1
- package/dist/builtin/subagents/agents/debugger.md +1 -1
- package/dist/builtin/subagents/agents/worker.md +1 -1
- package/dist/builtin/subagents/package.json +6 -5
- package/dist/builtin/subagents/src/agents/agent-discovery.ts +17 -11
- package/dist/builtin/subagents/src/agents/agent-loaders.ts +115 -43
- package/dist/builtin/subagents/src/agents/agent-management-helpers.ts +10 -8
- package/dist/builtin/subagents/src/agents/agent-management.ts +8 -7
- package/dist/builtin/subagents/src/agents/agent-overrides.ts +2 -2
- package/dist/builtin/subagents/src/agents/agent-scope.ts +1 -1
- package/dist/builtin/subagents/src/agents/agent-selection.ts +1 -1
- package/dist/builtin/subagents/src/agents/agent-serializer.ts +24 -3
- package/dist/builtin/subagents/src/agents/agent-types.ts +3 -1
- package/dist/builtin/subagents/src/agents/agents.ts +7 -5
- package/dist/builtin/subagents/src/agents/frontmatter.ts +67 -22
- package/dist/builtin/subagents/src/agents/identity.ts +1 -1
- package/dist/builtin/subagents/src/agents/skills-paths.ts +1 -1
- package/dist/builtin/subagents/src/agents/skills.ts +64 -6
- package/dist/builtin/subagents/src/extension/config.ts +1 -1
- package/dist/builtin/subagents/src/extension/control-notices.ts +2 -2
- package/dist/builtin/subagents/src/extension/doctor.ts +15 -5
- package/dist/builtin/subagents/src/extension/fanout-child.ts +12 -12
- package/dist/builtin/subagents/src/extension/index.ts +23 -23
- package/dist/builtin/subagents/src/extension/notification-content.ts +1 -1
- package/dist/builtin/subagents/src/extension/schemas.ts +1 -1
- package/dist/builtin/subagents/src/extension/startup-maintenance.ts +3 -3
- package/dist/builtin/subagents/src/intercom/intercom-bridge.ts +3 -3
- package/dist/builtin/subagents/src/intercom/result-intercom.ts +1 -1
- package/dist/builtin/subagents/src/intercom/supervisor-authorization.ts +1 -1
- package/dist/builtin/subagents/src/runs/foreground/completion-notification.ts +2 -2
- package/dist/builtin/subagents/src/runs/foreground/execution-detach-reservations.ts +1 -1
- package/dist/builtin/subagents/src/runs/foreground/execution-intercom-detach.ts +3 -3
- package/dist/builtin/subagents/src/runs/foreground/execution-run-sync.ts +1 -1
- package/dist/builtin/subagents/src/runs/foreground/execution-updates.ts +2 -2
- package/dist/builtin/subagents/src/runs/foreground/execution-utils.ts +1 -1
- package/dist/builtin/subagents/src/runs/foreground/execution.ts +2 -2
- package/dist/builtin/subagents/src/runs/foreground/inprocess-run-sync.ts +27 -11
- package/dist/builtin/subagents/src/runs/foreground/notify.ts +3 -3
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-context.ts +11 -11
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-input.ts +3 -3
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel-task.ts +15 -14
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel.ts +19 -19
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-runtime.ts +2 -2
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-single.ts +14 -13
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-status.ts +9 -9
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-types.ts +7 -7
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-worktree.ts +6 -6
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +24 -23
- package/dist/builtin/subagents/src/runs/inprocess/control-registry.ts +1 -1
- package/dist/builtin/subagents/src/runs/inprocess/control-status.ts +3 -3
- package/dist/builtin/subagents/src/runs/inprocess/index.ts +3 -3
- package/dist/builtin/subagents/src/runs/inprocess/prompt-behavior.ts +17 -8
- package/dist/builtin/subagents/src/runs/inprocess/runner.ts +60 -12
- package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-api.ts +8 -8
- package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-control.ts +3 -3
- package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-core.ts +4 -4
- package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-projection.ts +4 -4
- package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-registry.ts +5 -5
- package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-rendering.ts +3 -3
- package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-sanitize.ts +3 -3
- package/dist/builtin/subagents/src/runs/inprocess.ts +1 -1
- package/dist/builtin/subagents/src/runs/shared/long-running-guard.ts +1 -1
- package/dist/builtin/subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +1 -1
- package/dist/builtin/subagents/src/runs/shared/model-candidate-filter.ts +3 -3
- package/dist/builtin/subagents/src/runs/shared/model-fallback.ts +2 -2
- package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +1 -1
- package/dist/builtin/subagents/src/runs/shared/single-output.ts +1 -1
- package/dist/builtin/subagents/src/runs/shared/subagent-control.ts +1 -1
- package/dist/builtin/subagents/src/shared/artifacts.ts +2 -2
- package/dist/builtin/subagents/src/shared/event-jsonl-writer.ts +1 -1
- package/dist/builtin/subagents/src/shared/fast-mode.ts +1 -1
- package/dist/builtin/subagents/src/shared/formatters.ts +2 -2
- package/dist/builtin/subagents/src/shared/jsonl-writer.ts +1 -1
- package/dist/builtin/subagents/src/shared/model-resolution.ts +1 -1
- package/dist/builtin/subagents/src/shared/session-tokens.ts +1 -1
- package/dist/builtin/subagents/src/shared/settings.ts +4 -4
- package/dist/builtin/subagents/src/shared/status-format.ts +1 -1
- package/dist/builtin/subagents/src/shared/types-config.ts +16 -4
- package/dist/builtin/subagents/src/shared/types-depth.ts +1 -1
- package/dist/builtin/subagents/src/shared/types-nested.ts +1 -1
- package/dist/builtin/subagents/src/shared/types-output.ts +1 -1
- package/dist/builtin/subagents/src/shared/types-results.ts +1 -1
- package/dist/builtin/subagents/src/shared/types-runtime.ts +1 -1
- package/dist/builtin/subagents/src/shared/types.ts +6 -6
- package/dist/builtin/subagents/src/shared/utils.ts +3 -3
- package/dist/builtin/subagents/src/slash/prompt-template-bridge.ts +1 -1
- package/dist/builtin/subagents/src/slash/slash-bridge.ts +3 -3
- package/dist/builtin/subagents/src/slash/slash-commands.ts +6 -6
- package/dist/builtin/subagents/src/slash/slash-live-state.ts +3 -3
- package/dist/builtin/subagents/src/tui/render-event-formatting.ts +2 -2
- package/dist/builtin/subagents/src/tui/render-layout.ts +1 -1
- package/dist/builtin/subagents/src/tui/render-progress.ts +2 -2
- package/dist/builtin/subagents/src/tui/render-result-compact.ts +7 -7
- package/dist/builtin/subagents/src/tui/render-result.ts +10 -10
- package/dist/builtin/subagents/src/tui/render-stable-output.ts +1 -1
- package/dist/builtin/subagents/src/tui/render-status-progress.ts +5 -5
- package/dist/builtin/subagents/src/tui/render.ts +4 -4
- package/dist/builtin/web-access/package.json +2 -2
- package/dist/builtin/workflows/CHANGELOG.md +57 -0
- package/dist/builtin/workflows/README.md +39 -8
- package/dist/builtin/workflows/builtin/adversarial-verification-runner.ts +16 -5
- package/dist/builtin/workflows/builtin/adversarial-verification.ts +4 -0
- package/dist/builtin/workflows/builtin/classify-and-act.ts +4 -0
- package/dist/builtin/workflows/builtin/fan-out-and-synthesize.ts +4 -0
- package/dist/builtin/workflows/builtin/generate-and-filter-runner.ts +12 -4
- package/dist/builtin/workflows/builtin/generate-and-filter.ts +4 -0
- package/dist/builtin/workflows/builtin/goal-models.ts +8 -10
- package/dist/builtin/workflows/builtin/goal.ts +4 -0
- package/dist/builtin/workflows/builtin/index.d.ts +0 -4
- package/dist/builtin/workflows/builtin/loop-until-done-runner.ts +4 -2
- package/dist/builtin/workflows/builtin/loop-until-done.ts +4 -0
- package/dist/builtin/workflows/builtin/open-claude-design-live-protocol.ts +305 -0
- package/dist/builtin/workflows/builtin/open-claude-design-phases.ts +115 -171
- package/dist/builtin/workflows/builtin/open-claude-design-runner.ts +9 -25
- package/dist/builtin/workflows/builtin/open-claude-design-setup.ts +19 -86
- package/dist/builtin/workflows/builtin/open-claude-design-utils.ts +0 -6
- package/dist/builtin/workflows/builtin/open-claude-design.d.ts +0 -5
- package/dist/builtin/workflows/builtin/open-claude-design.ts +11 -15
- package/dist/builtin/workflows/builtin/ralph-models.ts +24 -29
- package/dist/builtin/workflows/builtin/ralph.ts +4 -0
- package/dist/builtin/workflows/builtin/tournament-runner.ts +6 -3
- package/dist/builtin/workflows/builtin/tournament.ts +4 -0
- package/dist/builtin/workflows/package.json +2 -2
- package/dist/builtin/workflows/skills/impeccable/SKILL.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_asset_producer.toml +25 -31
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_documenter.toml +1 -0
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_finish_reviewer.toml +16 -10
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_manual_edit_applier.toml +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/android.md +6 -0
- package/dist/builtin/workflows/skills/impeccable/reference/animate.md +3 -0
- package/dist/builtin/workflows/skills/impeccable/reference/bolder.md +3 -1
- package/dist/builtin/workflows/skills/impeccable/reference/craft-floor.md +6 -1
- package/dist/builtin/workflows/skills/impeccable/reference/critique.md +18 -2
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/asset-producer.md +24 -30
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/documenter.md +1 -0
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/finish-reviewer.md +16 -10
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/manual-edit-applier.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/distill.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/doctor.md +1 -0
- package/dist/builtin/workflows/skills/impeccable/reference/document.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/extract.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/hooks.md +12 -6
- package/dist/builtin/workflows/skills/impeccable/reference/init.md +12 -3
- package/dist/builtin/workflows/skills/impeccable/reference/ios.md +6 -0
- package/dist/builtin/workflows/skills/impeccable/reference/live-setup.md +102 -0
- package/dist/builtin/workflows/skills/impeccable/reference/live.md +113 -521
- package/dist/builtin/workflows/skills/impeccable/reference/new-work.md +39 -26
- package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/polish.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/visualize.md +25 -11
- package/dist/builtin/workflows/skills/impeccable/scripts/concept-seed.mjs +332 -221
- package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +10 -19
- package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +91 -38
- package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +19 -10
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +56 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +10 -16
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/design-system.mjs +140 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +1174 -694
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns.mjs +1 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +5 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +431 -45
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +10 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +47 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +23 -22
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +0 -10
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +384 -384
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/color.mjs +466 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/doctor.mjs +2 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/embed-prompt.mjs +270 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/generate-image.mjs +51 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/hook-admin.mjs +70 -10
- package/dist/builtin/workflows/skills/impeccable/scripts/hook-lib.mjs +336 -90
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/composition-catalog.mjs +38 -3
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/concept-catalog.mjs +77 -11
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/design-parser.mjs +102 -19
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-config.mjs +44 -59
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/is-generated.mjs +3 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/open-system-browser.mjs +57 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/roll-selection.mjs +369 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness-deep.mjs +31 -3
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness.mjs +73 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/live/accept-css.mjs +617 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/accept-verify.mjs +71 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/browser-script-parts.mjs +29 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/live/event-validation.mjs +54 -7
- package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/astro.mjs +47 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/detect-utils.mjs +157 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/index.mjs +143 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/journal.mjs +205 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/nextjs.mjs +49 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/nuxt.mjs +170 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/script-src.mjs +26 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/static-html.mjs +26 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/sveltekit.mjs +71 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/tag-strategy.mjs +247 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/tanstack-start.mjs +70 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/frameworks/vite-generic.mjs +42 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/instructions.mjs +169 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/roots.mjs +670 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/session-store.mjs +232 -35
- package/dist/builtin/workflows/skills/impeccable/scripts/live/svelte-ast.mjs +969 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/svelte-component.mjs +621 -74
- package/dist/builtin/workflows/skills/impeccable/scripts/live/sveltekit-adapter.mjs +66 -28
- package/dist/builtin/workflows/skills/impeccable/scripts/live/tanstack-adapter.mjs +12 -12
- package/dist/builtin/workflows/skills/impeccable/scripts/live/ui-surfaces.mjs +75 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/vocabulary.mjs +135 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +23 -37
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +945 -115
- package/dist/builtin/workflows/skills/impeccable/scripts/live-complete.mjs +33 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live-copy-edit-agent.mjs +132 -15
- package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +173 -425
- package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +2 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +16 -3
- package/dist/builtin/workflows/skills/impeccable/scripts/live-resume.mjs +39 -10
- package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +194 -27
- package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +10 -4
- package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +50 -31
- package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +91 -28
- package/dist/builtin/workflows/skills/impeccable/scripts/pin.mjs +8 -5
- package/dist/builtin/workflows/skills/impeccable/scripts/serve-question.mjs +759 -109
- package/dist/builtin/workflows/src/authoring/workflow.ts +9 -0
- package/dist/builtin/workflows/src/authoring.d.ts +3 -0
- package/dist/builtin/workflows/src/durable/completed-catalog.ts +23 -2
- package/dist/builtin/workflows/src/durable/dbos-backend.ts +30 -17
- package/dist/builtin/workflows/src/durable/dbos-envelope.ts +14 -0
- package/dist/builtin/workflows/src/durable/dbos-lifecycle.ts +60 -57
- package/dist/builtin/workflows/src/durable/dbos-process-owner.ts +87 -0
- package/dist/builtin/workflows/src/durable/dbos-registration-diagnostics.ts +113 -0
- package/dist/builtin/workflows/src/durable/factory.ts +11 -8
- package/dist/builtin/workflows/src/durable/tool-failure-checkpoint.ts +5 -0
- package/dist/builtin/workflows/src/durable/tool-primitive.ts +186 -20
- package/dist/builtin/workflows/src/durable/types.ts +4 -0
- package/dist/builtin/workflows/src/durable/workflow-heartbeat-anchor.ts +153 -0
- package/dist/builtin/workflows/src/engine/run-durable-stage-session.ts +28 -0
- package/dist/builtin/workflows/src/engine/run-tool-control-registry.ts +9 -1
- package/dist/builtin/workflows/src/engine/run.ts +1 -0
- package/dist/builtin/workflows/src/extension/adopt-session-run-state.ts +20 -0
- package/dist/builtin/workflows/src/extension/background-ui-adapter.ts +9 -30
- package/dist/builtin/workflows/src/extension/extension-factory.ts +3 -0
- package/dist/builtin/workflows/src/extension/extension-lifecycle.ts +28 -10
- package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +138 -0
- package/dist/builtin/workflows/src/extension/index.bundle.mjs +3365 -1341
- package/dist/builtin/workflows/src/extension/workflow-heartbeat-delivery.ts +197 -0
- package/dist/builtin/workflows/src/extension/workflow-heartbeat-notice.ts +115 -0
- package/dist/builtin/workflows/src/extension/workflow-heartbeat-scheduler.ts +962 -0
- package/dist/builtin/workflows/src/extension/workflow-prompts.ts +4 -1
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +29 -5
- package/dist/builtin/workflows/src/extension/workflow-tool-send.ts +32 -1
- package/dist/builtin/workflows/src/runs/background/cancellation-registry.ts +10 -1
- package/dist/builtin/workflows/src/runs/background/job-tracker.ts +10 -1
- package/dist/builtin/workflows/src/runs/foreground/executor-prompt-nodes.ts +16 -11
- package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +31 -1
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +41 -10
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-messages.ts +48 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-output.ts +4 -1
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-structured-output.ts +45 -12
- package/dist/builtin/workflows/src/sdk-surface.ts +9 -0
- package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +10 -0
- package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +10 -0
- package/dist/builtin/workflows/src/shared/graph-store-snapshot.ts +14 -2
- package/dist/builtin/workflows/src/shared/persistence-restore.ts +12 -0
- package/dist/builtin/workflows/src/shared/prompt-answer.ts +130 -0
- package/dist/builtin/workflows/src/shared/session-scoped-singleton.ts +98 -0
- package/dist/builtin/workflows/src/shared/stage-ui-broker.ts +9 -1
- package/dist/builtin/workflows/src/shared/store-factory.ts +9 -1
- package/dist/builtin/workflows/src/shared/store-public-types.ts +2 -1
- package/dist/builtin/workflows/src/shared/store-tool-node-methods.ts +7 -0
- package/dist/builtin/workflows/src/shared/store-types.ts +18 -1
- package/dist/builtin/workflows/src/shared/tool-payload-bounds.ts +576 -0
- package/dist/builtin/workflows/src/shared/workflow-authoring-types.d.ts +1 -0
- package/dist/builtin/workflows/src/shared/workflow-authoring-types.ts +1 -0
- package/dist/builtin/workflows/src/shared/workflow-heartbeat-contract.d.ts +21 -0
- package/dist/builtin/workflows/src/shared/workflow-heartbeat-contract.ts +25 -0
- package/dist/builtin/workflows/src/tui/graph-theme.ts +8 -1
- package/dist/builtin/workflows/src/tui/graph-view-constants.ts +11 -0
- package/dist/builtin/workflows/src/tui/graph-view-input.ts +100 -5
- package/dist/builtin/workflows/src/tui/graph-view-layout.ts +42 -11
- package/dist/builtin/workflows/src/tui/graph-view-render-helpers.ts +55 -6
- package/dist/builtin/workflows/src/tui/graph-view-render.ts +71 -6
- package/dist/builtin/workflows/src/tui/graph-view-state.ts +43 -0
- package/dist/builtin/workflows/src/tui/keybindings-adapter.ts +2 -0
- package/dist/builtin/workflows/src/tui/node-card.ts +5 -1
- package/dist/builtin/workflows/src/tui/stage-chat-view-archive-history.ts +11 -2
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +36 -10
- package/dist/builtin/workflows/src/tui/switcher.ts +11 -4
- package/dist/builtin/workflows/src/tui/tool-detail.ts +348 -0
- package/dist/builtin/workflows/src/tui/workflow-status.ts +5 -2
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +11 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/auth-check.d.ts +1 -1
- package/dist/cli/auth-check.js.map +1 -1
- package/dist/cli/credential-print.d.ts +1 -1
- package/dist/cli/credential-print.js.map +1 -1
- package/dist/cli/list-models.d.ts +1 -1
- package/dist/cli/list-models.js.map +1 -1
- package/dist/cli/startup-ui.d.ts +9 -0
- package/dist/cli/startup-ui.d.ts.map +1 -1
- package/dist/cli/startup-ui.js +7 -1
- package/dist/cli/startup-ui.js.map +1 -1
- package/dist/core/agent-session-custom-message-commit.js +2 -2
- package/dist/core/agent-session-custom-message-commit.js.map +1 -1
- package/dist/core/agent-session-export.d.ts +6 -1
- package/dist/core/agent-session-export.d.ts.map +1 -1
- package/dist/core/agent-session-export.js +6 -3
- package/dist/core/agent-session-export.js.map +1 -1
- package/dist/core/agent-session-extension-bindings.d.ts.map +1 -1
- package/dist/core/agent-session-extension-bindings.js +6 -4
- package/dist/core/agent-session-extension-bindings.js.map +1 -1
- package/dist/core/agent-session-methods.d.ts +5 -2
- package/dist/core/agent-session-methods.d.ts.map +1 -1
- package/dist/core/agent-session-methods.js.map +1 -1
- package/dist/core/agent-session-prompt.d.ts +12 -1
- package/dist/core/agent-session-prompt.d.ts.map +1 -1
- package/dist/core/agent-session-prompt.js +31 -10
- package/dist/core/agent-session-prompt.js.map +1 -1
- package/dist/core/agent-session-runtime-auth.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +1 -1
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session-skill-block.d.ts +1 -0
- package/dist/core/agent-session-skill-block.d.ts.map +1 -1
- package/dist/core/agent-session-skill-block.js +18 -2
- package/dist/core/agent-session-skill-block.js.map +1 -1
- package/dist/core/agent-session-state.d.ts.map +1 -1
- package/dist/core/agent-session-state.js +2 -1
- package/dist/core/agent-session-state.js.map +1 -1
- package/dist/core/agent-session-types.d.ts +2 -1
- package/dist/core/agent-session-types.d.ts.map +1 -1
- package/dist/core/agent-session-types.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/atomic-guide-command.js +2 -2
- package/dist/core/atomic-guide-command.js.map +1 -1
- package/dist/core/builtin-packages.d.ts.map +1 -1
- package/dist/core/builtin-packages.js +0 -6
- package/dist/core/builtin-packages.js.map +1 -1
- package/dist/core/cloudflare-gateway-binding.d.ts +11 -0
- package/dist/core/cloudflare-gateway-binding.d.ts.map +1 -0
- package/dist/core/cloudflare-gateway-binding.js +11 -0
- package/dist/core/cloudflare-gateway-binding.js.map +1 -0
- package/dist/core/codex-fast-mode-transport.d.ts +12 -0
- package/dist/core/codex-fast-mode-transport.d.ts.map +1 -0
- package/dist/core/codex-fast-mode-transport.js +130 -0
- package/dist/core/codex-fast-mode-transport.js.map +1 -0
- package/dist/core/codex-fast-mode.d.ts +20 -6
- package/dist/core/codex-fast-mode.d.ts.map +1 -1
- package/dist/core/codex-fast-mode.js +111 -3
- package/dist/core/codex-fast-mode.js.map +1 -1
- package/dist/core/diagnostics.d.ts +4 -0
- package/dist/core/diagnostics.d.ts.map +1 -1
- package/dist/core/diagnostics.js.map +1 -1
- package/dist/core/event-bus.d.ts +12 -0
- package/dist/core/event-bus.d.ts.map +1 -1
- package/dist/core/event-bus.js +22 -0
- package/dist/core/event-bus.js.map +1 -1
- package/dist/core/experimental.d.ts +25 -0
- package/dist/core/experimental.d.ts.map +1 -1
- package/dist/core/experimental.js +24 -0
- package/dist/core/experimental.js.map +1 -1
- package/dist/core/export-html/template-js/tree-filter-render.js +4 -3
- package/dist/core/export-html/template.js +4 -3
- package/dist/core/extension-session-state.d.ts +28 -0
- package/dist/core/extension-session-state.d.ts.map +1 -0
- package/dist/core/extension-session-state.js +55 -0
- package/dist/core/extension-session-state.js.map +1 -0
- package/dist/core/extensions/api-types.d.ts +6 -0
- package/dist/core/extensions/api-types.d.ts.map +1 -1
- package/dist/core/extensions/api-types.js.map +1 -1
- package/dist/core/extensions/context-types.d.ts +4 -0
- package/dist/core/extensions/context-types.d.ts.map +1 -1
- package/dist/core/extensions/context-types.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -0
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js +1 -0
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader-api.d.ts +1 -1
- package/dist/core/extensions/loader-api.d.ts.map +1 -1
- package/dist/core/extensions/loader-api.js +16 -10
- package/dist/core/extensions/loader-api.js.map +1 -1
- package/dist/core/extensions/loader-virtual-modules.d.ts.map +1 -1
- package/dist/core/extensions/loader-virtual-modules.js +12 -1
- package/dist/core/extensions/loader-virtual-modules.js.map +1 -1
- package/dist/core/extensions/runner-context.d.ts +2 -0
- package/dist/core/extensions/runner-context.d.ts.map +1 -1
- package/dist/core/extensions/runner-context.js +8 -0
- package/dist/core/extensions/runner-context.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +1 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +3 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/runtime-types.d.ts +3 -0
- package/dist/core/extensions/runtime-types.d.ts.map +1 -1
- package/dist/core/extensions/runtime-types.js.map +1 -1
- package/dist/core/extensions/ui-types.d.ts +26 -0
- package/dist/core/extensions/ui-types.d.ts.map +1 -1
- package/dist/core/extensions/ui-types.js +17 -1
- package/dist/core/extensions/ui-types.js.map +1 -1
- package/dist/core/http-dispatcher.d.ts.map +1 -1
- package/dist/core/http-dispatcher.js +5 -0
- package/dist/core/http-dispatcher.js.map +1 -1
- package/dist/core/keybindings.d.ts +24 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +6 -0
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/model-config.d.ts +5 -0
- package/dist/core/model-config.d.ts.map +1 -1
- package/dist/core/model-config.js +1 -0
- package/dist/core/model-config.js.map +1 -1
- package/dist/core/model-registry.d.ts +1 -1
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver-cli.d.ts +1 -1
- package/dist/core/model-resolver-cli.js.map +1 -1
- package/dist/core/model-resolver-defaults.js +3 -3
- package/dist/core/model-resolver-defaults.js.map +1 -1
- package/dist/core/model-resolver-initial.d.ts +1 -1
- package/dist/core/model-resolver-initial.js.map +1 -1
- package/dist/core/model-resolver-scope.d.ts +1 -1
- package/dist/core/model-resolver-scope.js.map +1 -1
- package/dist/core/model-runtime.d.ts +54 -0
- package/dist/core/model-runtime.d.ts.map +1 -1
- package/dist/core/model-runtime.js +96 -13
- package/dist/core/model-runtime.js.map +1 -1
- package/dist/core/oauth-login.js.map +1 -1
- package/dist/core/project-trust.d.ts +10 -0
- package/dist/core/project-trust.d.ts.map +1 -1
- package/dist/core/project-trust.js +12 -0
- package/dist/core/project-trust.js.map +1 -1
- package/dist/core/resource-loader-assets.d.ts.map +1 -1
- package/dist/core/resource-loader-assets.js +14 -5
- package/dist/core/resource-loader-assets.js.map +1 -1
- package/dist/core/resource-loader-core.d.ts +3 -0
- package/dist/core/resource-loader-core.d.ts.map +1 -1
- package/dist/core/resource-loader-core.js +5 -0
- package/dist/core/resource-loader-core.js.map +1 -1
- package/dist/core/resource-loader-internals.d.ts +2 -0
- package/dist/core/resource-loader-internals.d.ts.map +1 -1
- package/dist/core/resource-loader-internals.js.map +1 -1
- package/dist/core/resource-loader-reload.d.ts.map +1 -1
- package/dist/core/resource-loader-reload.js +2 -0
- package/dist/core/resource-loader-reload.js.map +1 -1
- package/dist/core/resource-loader-types.d.ts +2 -0
- package/dist/core/resource-loader-types.d.ts.map +1 -1
- package/dist/core/resource-loader-types.js.map +1 -1
- package/dist/core/resource-loader.d.ts +2 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +1 -0
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk-types.d.ts +7 -6
- package/dist/core/sdk-types.d.ts.map +1 -1
- package/dist/core/sdk-types.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +17 -6
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager-core.d.ts +7 -1
- package/dist/core/session-manager-core.d.ts.map +1 -1
- package/dist/core/session-manager-core.js +11 -2
- package/dist/core/session-manager-core.js.map +1 -1
- package/dist/core/session-manager-entries.d.ts +2 -2
- package/dist/core/session-manager-entries.d.ts.map +1 -1
- package/dist/core/session-manager-entries.js +6 -4
- package/dist/core/session-manager-entries.js.map +1 -1
- package/dist/core/session-manager-list.d.ts.map +1 -1
- package/dist/core/session-manager-list.js +5 -1
- package/dist/core/session-manager-list.js.map +1 -1
- package/dist/core/session-manager-types.d.ts +25 -2
- package/dist/core/session-manager-types.d.ts.map +1 -1
- package/dist/core/session-manager-types.js.map +1 -1
- package/dist/core/session-manager.d.ts +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager-basic-accessors.d.ts +1 -0
- package/dist/core/settings-manager-basic-accessors.d.ts.map +1 -1
- package/dist/core/settings-manager-basic-accessors.js +8 -0
- package/dist/core/settings-manager-basic-accessors.js.map +1 -1
- package/dist/core/settings-manager-ui-accessors.d.ts +3 -1
- package/dist/core/settings-manager-ui-accessors.d.ts.map +1 -1
- package/dist/core/settings-manager-ui-accessors.js +9 -0
- package/dist/core/settings-manager-ui-accessors.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/settings-types.d.ts +4 -0
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/skill-catalog.d.ts +35 -0
- package/dist/core/skill-catalog.d.ts.map +1 -0
- package/dist/core/skill-catalog.js +264 -0
- package/dist/core/skill-catalog.js.map +1 -0
- package/dist/core/skills-async.d.ts.map +1 -1
- package/dist/core/skills-async.js +12 -6
- package/dist/core/skills-async.js.map +1 -1
- package/dist/core/skills.d.ts +2 -0
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +8 -5
- package/dist/core/skills.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -5
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.js +2 -2
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/ask-user-question/ask-user-question.d.ts +25 -2
- package/dist/core/tools/ask-user-question/ask-user-question.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/ask-user-question.js +47 -18
- package/dist/core/tools/ask-user-question/ask-user-question.js.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/multi-select-view.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/multi-select-view.js +5 -2
- package/dist/core/tools/ask-user-question/view/components/multi-select-view.js.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/submit-picker.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/submit-picker.js +3 -1
- package/dist/core/tools/ask-user-question/view/components/submit-picker.js.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/wrapping-select.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/wrapping-select.js +8 -1
- package/dist/core/tools/ask-user-question/view/components/wrapping-select.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +2 -0
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +2 -0
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +3 -1
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.js +1 -1
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/hashline-engine/format.d.ts +2 -0
- package/dist/core/tools/hashline-engine/format.d.ts.map +1 -1
- package/dist/core/tools/hashline-engine/format.js +4 -2
- package/dist/core/tools/hashline-engine/format.js.map +1 -1
- package/dist/core/tools/hashline-engine/messages.d.ts +2 -0
- package/dist/core/tools/hashline-engine/messages.d.ts.map +1 -1
- package/dist/core/tools/hashline-engine/messages.js +4 -2
- package/dist/core/tools/hashline-engine/messages.js.map +1 -1
- package/dist/core/tools/hashline-engine/parser.d.ts.map +1 -1
- package/dist/core/tools/hashline-engine/parser.js +16 -4
- package/dist/core/tools/hashline-engine/parser.js.map +1 -1
- package/dist/core/tools/hashline-engine/tokenizer.d.ts.map +1 -1
- package/dist/core/tools/hashline-engine/tokenizer.js +37 -27
- package/dist/core/tools/hashline-engine/tokenizer.js.map +1 -1
- package/dist/core/tools/hashline.d.ts.map +1 -1
- package/dist/core/tools/hashline.js +8 -2
- package/dist/core/tools/hashline.js.map +1 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +2 -0
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +8 -1
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/search.d.ts.map +1 -1
- package/dist/core/tools/search.js +2 -0
- package/dist/core/tools/search.js.map +1 -1
- package/dist/core/tools/todos.d.ts +3 -12
- package/dist/core/tools/todos.d.ts.map +1 -1
- package/dist/core/tools/todos.js +19 -4
- package/dist/core/tools/todos.js.map +1 -1
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +2 -0
- package/dist/core/tools/write.js.map +1 -1
- package/dist/index-extensions.d.ts +1 -1
- package/dist/index-extensions.d.ts.map +1 -1
- package/dist/index-extensions.js +1 -1
- package/dist/index-extensions.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/main-runtime-api-key.d.ts +1 -1
- package/dist/main-runtime-api-key.js.map +1 -1
- package/dist/main-session-options.d.ts +1 -1
- package/dist/main-session-options.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +10 -2
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-rendering.js +1 -1
- package/dist/modes/interactive/components/chat-session-host-rendering.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-state.d.ts +8 -0
- package/dist/modes/interactive/components/chat-session-host-state.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-state.js +8 -0
- package/dist/modes/interactive/components/chat-session-host-state.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.d.ts +26 -0
- package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.js +38 -0
- package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
- package/dist/modes/interactive/components/chat-transcript.d.ts +37 -0
- package/dist/modes/interactive/components/chat-transcript.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-transcript.js +54 -1
- package/dist/modes/interactive/components/chat-transcript.js.map +1 -1
- package/dist/modes/interactive/components/fast-mode-selector.js +1 -1
- package/dist/modes/interactive/components/fast-mode-selector.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +2 -1
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts +1 -1
- package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
- package/dist/modes/interactive/components/reserved-bottom-overlay.d.ts +95 -0
- package/dist/modes/interactive/components/reserved-bottom-overlay.d.ts.map +1 -0
- package/dist/modes/interactive/components/reserved-bottom-overlay.js +234 -0
- package/dist/modes/interactive/components/reserved-bottom-overlay.js.map +1 -0
- package/dist/modes/interactive/components/session-selector-search.d.ts +4 -0
- package/dist/modes/interactive/components/session-selector-search.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector-search.js +4 -0
- package/dist/modes/interactive/components/session-selector-search.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector-handlers.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector-handlers.js +3 -0
- package/dist/modes/interactive/components/settings-selector-handlers.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector-items.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector-items.js +7 -0
- package/dist/modes/interactive/components/settings-selector-items.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector-types.d.ts +3 -1
- package/dist/modes/interactive/components/settings-selector-types.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector-types.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +12 -1
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-auth-login.js.map +1 -1
- package/dist/modes/interactive/interactive-auth-routing.js.map +1 -1
- package/dist/modes/interactive/interactive-autocomplete.js +5 -4
- package/dist/modes/interactive/interactive-autocomplete.js.map +1 -1
- package/dist/modes/interactive/interactive-bash-compact.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-bash-compact.js +2 -2
- package/dist/modes/interactive/interactive-bash-compact.js.map +1 -1
- package/dist/modes/interactive/interactive-extension-custom-ui.js +94 -6
- package/dist/modes/interactive/interactive-extension-custom-ui.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-base.d.ts +41 -3
- package/dist/modes/interactive/interactive-mode-base.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode-base.js +80 -11
- package/dist/modes/interactive/interactive-mode-base.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-surface.d.ts +9 -1
- package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-types.d.ts +2 -0
- package/dist/modes/interactive/interactive-mode-types.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode-types.js.map +1 -1
- package/dist/modes/interactive/interactive-model-catalog-startup.d.ts +8 -1
- package/dist/modes/interactive/interactive-model-catalog-startup.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-model-catalog-startup.js +13 -3
- package/dist/modes/interactive/interactive-model-catalog-startup.js.map +1 -1
- package/dist/modes/interactive/interactive-model-routing.js +3 -4
- package/dist/modes/interactive/interactive-model-routing.js.map +1 -1
- package/dist/modes/interactive/interactive-render-chat.js +6 -0
- package/dist/modes/interactive/interactive-render-chat.js.map +1 -1
- package/dist/modes/interactive/interactive-resource-disclosure.js +2 -2
- package/dist/modes/interactive/interactive-resource-disclosure.js.map +1 -1
- package/dist/modes/interactive/interactive-selectors.js +7 -3
- package/dist/modes/interactive/interactive-selectors.js.map +1 -1
- package/dist/modes/interactive/interactive-session-runtime.js +7 -1
- package/dist/modes/interactive/interactive-session-runtime.js.map +1 -1
- package/dist/modes/interactive/interactive-slash-commands.js +2 -2
- package/dist/modes/interactive/interactive-slash-commands.js.map +1 -1
- package/dist/modes/interactive/interactive-startup.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-startup.js +34 -9
- package/dist/modes/interactive/interactive-startup.js.map +1 -1
- package/dist/modes/interactive/interactive-tui.d.ts +21 -1
- package/dist/modes/interactive/interactive-tui.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-tui.js +140 -38
- package/dist/modes/interactive/interactive-tui.js.map +1 -1
- package/dist/modes/interactive/model-catalog-refresh.d.ts +17 -0
- package/dist/modes/interactive/model-catalog-refresh.d.ts.map +1 -0
- package/dist/modes/interactive/model-catalog-refresh.js +84 -0
- package/dist/modes/interactive/model-catalog-refresh.js.map +1 -0
- package/dist/modes/interactive/theme/dark.json +2 -0
- package/dist/modes/interactive/theme/light.json +2 -0
- package/dist/modes/interactive/theme/theme-class.d.ts +12 -3
- package/dist/modes/interactive/theme/theme-class.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme-class.js +6 -1
- package/dist/modes/interactive/theme/theme-class.js.map +1 -1
- package/dist/modes/interactive/theme/theme-controller.d.ts +10 -2
- package/dist/modes/interactive/theme/theme-controller.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme-controller.js +23 -10
- package/dist/modes/interactive/theme/theme-controller.js.map +1 -1
- package/dist/modes/interactive/theme/theme-loading.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme-loading.js +1 -0
- package/dist/modes/interactive/theme/theme-loading.js.map +1 -1
- package/dist/modes/interactive/theme/theme-schema.d.ts +8 -0
- package/dist/modes/interactive/theme/theme-schema.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme-schema.js +3 -1
- package/dist/modes/interactive/theme/theme-schema.js.map +1 -1
- package/dist/modes/interactive/theme/theme-schema.json +9 -1
- package/dist/modes/interactive-engine/engine-custom-ui.d.ts +2 -0
- package/dist/modes/interactive-engine/engine-custom-ui.d.ts.map +1 -1
- package/dist/modes/interactive-engine/engine-custom-ui.js +1 -0
- package/dist/modes/interactive-engine/engine-custom-ui.js.map +1 -1
- package/dist/modes/interactive-engine/protocol.d.ts +1 -0
- package/dist/modes/interactive-engine/protocol.d.ts.map +1 -1
- package/dist/modes/interactive-engine/protocol.js +1 -0
- package/dist/modes/interactive-engine/protocol.js.map +1 -1
- package/dist/modes/interactive-engine/remote-component.d.ts +3 -1
- package/dist/modes/interactive-engine/remote-component.d.ts.map +1 -1
- package/dist/modes/interactive-engine/remote-component.js +3 -2
- package/dist/modes/interactive-engine/remote-component.js.map +1 -1
- package/dist/modes/json-event.d.ts +6 -1
- package/dist/modes/json-event.d.ts.map +1 -1
- package/dist/modes/json-event.js +17 -2
- package/dist/modes/json-event.js.map +1 -1
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-command-handler.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-command-handler.js +5 -4
- package/dist/modes/rpc/rpc-command-handler.js.map +1 -1
- package/dist/modes/rpc/rpc-provider-auth.js.map +1 -1
- package/dist/modes/rpc/rpc-responses.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/server/create-harness.d.ts.map +1 -1
- package/dist/server/create-harness.js +11 -0
- package/dist/server/create-harness.js.map +1 -1
- package/dist/utils/abort.d.ts +9 -0
- package/dist/utils/abort.d.ts.map +1 -0
- package/dist/utils/abort.js +19 -0
- package/dist/utils/abort.js.map +1 -0
- package/dist/utils/tools-manager.d.ts +16 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +19 -19
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/custom-provider.md +9 -8
- package/docs/development.md +0 -1
- package/docs/docs.json +0 -1
- package/docs/environment-variables.md +3 -0
- package/docs/extensions.md +100 -2
- package/docs/json.md +3 -1
- package/docs/keybindings.md +8 -3
- package/docs/models/artificial-analysis-index.md +2 -2
- package/docs/models/model-selection.md +32 -23
- package/docs/models/pareto-efficiency.md +25 -24
- package/docs/providers.md +95 -2
- package/docs/quickstart.md +4 -2
- package/docs/rpc.md +21 -7
- package/docs/sdk.md +26 -1
- package/docs/session-format.md +5 -2
- package/docs/settings.md +34 -3
- package/docs/skills.md +16 -1
- package/docs/terminal-setup.md +15 -2
- package/docs/themes.md +22 -4
- package/docs/tools.md +3 -1
- package/docs/tui.md +2 -0
- package/docs/usage.md +5 -4
- package/docs/workflows.md +96 -19
- package/npm-shrinkwrap.json +211 -232
- package/package.json +8 -8
- package/dist/builtin/i-have-adhd/CHANGELOG.md +0 -30
- package/dist/builtin/i-have-adhd/LICENSE +0 -21
- package/dist/builtin/i-have-adhd/README.md +0 -18
- package/dist/builtin/i-have-adhd/index.ts +0 -238
- package/dist/builtin/i-have-adhd/package.json +0 -56
- package/dist/builtin/i-have-adhd/skills/i-have-adhd/SKILL.md +0 -140
- package/dist/builtin/workflows/builtin/open-claude-design-feedback.ts +0 -359
- package/dist/builtin/workflows/skills/impeccable/scripts/live/ui-core.mjs +0 -180
- package/docs/i-have-adhd.md +0 -36
|
@@ -29,28 +29,53 @@
|
|
|
29
29
|
* "materials": ["letterpress", "newsprint"], // optional, rendered as tags
|
|
30
30
|
* "viewport": "one line: the first-viewport composition", // optional
|
|
31
31
|
* "case": "one line: the fusion verdict, honest", // optional
|
|
32
|
+
* "verdict": "competitive", // optional routing tier: "wins" |
|
|
33
|
+
* // "competitive" | "declined". Declined cards
|
|
34
|
+
* // render demoted after the full cards:
|
|
35
|
+
* // narrow, quiet, catalog art as a labeled
|
|
36
|
+
* // thumb, "Adopt anyway" instead of "Build
|
|
37
|
+
* // this". Still choosable; never deleted.
|
|
38
|
+
* "kept": "one line: what the direction kept from this declined world",
|
|
39
|
+
* "raised": [ { "from": "challenger-x", "raise": "one line" } ],
|
|
40
|
+
* // assigned card only: donations taken from
|
|
41
|
+
* // declined challengers, rendered as named
|
|
42
|
+
* // raise lines under the identity row
|
|
32
43
|
* "risk": "one line: the honest risk", // optional
|
|
33
44
|
* "body": "fallback prose when the structured fields are absent",
|
|
34
|
-
* "
|
|
35
|
-
* //
|
|
36
|
-
* //
|
|
37
|
-
* //
|
|
45
|
+
* "comp": ".impeccable/mocks/decision/assigned.webp", // optional; the card's
|
|
46
|
+
* // full-fidelity direction comp (the legacy
|
|
47
|
+
* // key "sketch" is accepted as an alias). May
|
|
48
|
+
* // not exist yet: the page shimmer-waits and
|
|
49
|
+
* // polls the slot until the file lands, so
|
|
50
|
+
* // serve first and generate after
|
|
38
51
|
* "hero": "https://... or /abs/path.webp", // optional inspiration image;
|
|
39
|
-
* // rides picture-in-picture when a
|
|
52
|
+
* // rides picture-in-picture when a comp exists
|
|
40
53
|
* "board": "https://... or /abs/path.webp" // optional secondary image
|
|
41
54
|
* }, ...
|
|
42
55
|
* ],
|
|
43
56
|
* "reroll": true, // adds a re-roll action (returns {"optionId":"reroll"})
|
|
57
|
+
* // or { "registers": ["safer", "bolder"] } to add
|
|
58
|
+
* // the register steers beside it: the answer then
|
|
59
|
+
* // carries "register" and the agent re-runs
|
|
60
|
+
* // concept-seed with --register <value>
|
|
44
61
|
* "canon": true, // adds the "Play it straight" standing exit;
|
|
45
62
|
* // direction rounds only (returns {"optionId":"canon"})
|
|
46
63
|
* "canonCard": { ... }, // optional: the standing exit as a full card with the
|
|
47
|
-
* // same anatomy (label, thesis, palette,
|
|
64
|
+
* // same anatomy (label, thesis, palette, comp, ...);
|
|
48
65
|
* // rendered last and visually subordinate. Without it,
|
|
49
66
|
* // canon stays a quiet footer action.
|
|
50
|
-
* "steer": true
|
|
67
|
+
* "steer": true, // adds a free-text steer field returned with any answer
|
|
68
|
+
* "followup": true // this round's pick is not terminal: the server
|
|
69
|
+
* // stays open awaiting --update with the next
|
|
70
|
+
* // round (detached mode only), the page shows a
|
|
71
|
+
* // loading hand instead of goodbye, and the
|
|
72
|
+
* // answer carries followup:true so --wait knows
|
|
73
|
+
* // to keep the table. Use it when a decision has
|
|
74
|
+
* // a known second half, e.g. direction first,
|
|
75
|
+
* // then the execution contract.
|
|
51
76
|
* }
|
|
52
77
|
*
|
|
53
|
-
* Options render as large cards: the
|
|
78
|
+
* Options render as large cards: the comp leads when present, with the
|
|
54
79
|
* inspiration image picture-in-picture; a hero alone renders full-bleed; a
|
|
55
80
|
* text-only direction gets its identity from the palette chips and tags.
|
|
56
81
|
* Local image paths are served by this server; nothing is uploaded anywhere.
|
|
@@ -79,6 +104,7 @@ import fs from 'node:fs';
|
|
|
79
104
|
import path from 'node:path';
|
|
80
105
|
import { spawn } from 'node:child_process';
|
|
81
106
|
import { fileURLToPath } from 'node:url';
|
|
107
|
+
import { openSystemBrowser } from './lib/open-system-browser.mjs';
|
|
82
108
|
|
|
83
109
|
function arg(name, fallback = null) {
|
|
84
110
|
const i = process.argv.indexOf(`--${name}`);
|
|
@@ -122,12 +148,30 @@ function printAnswer(raw) {
|
|
|
122
148
|
if (a.hero || a.board) {
|
|
123
149
|
console.log("CHOSEN CARD: open the chosen world's board and hero images now, before any code. When your harness only reads files, or runs sandboxed, download them INTO the workspace and open the relative path; a sandboxed viewer rejects absolute paths outside it. They set the craft bar the build must reach.");
|
|
124
150
|
}
|
|
125
|
-
if (a.
|
|
126
|
-
console.log('CHOSEN
|
|
151
|
+
if (a.comp) {
|
|
152
|
+
console.log('CHOSEN COMP: the decision comp at that path is compositional option one. On a comp-led build the comp round adds two variations beside it; on a code-led build it returns at the finish review as the critique reference. Never regenerate it from scratch.');
|
|
127
153
|
}
|
|
128
154
|
if (a.optionId === 'canon') {
|
|
129
155
|
console.log('CANON CHOSEN: the user picked the category standard on purpose. Ask once for two or three products this should sit alongside; their craft level becomes the quality bar. Execute the canon at full commitment, conventions embraced without irony or smuggled quirk.');
|
|
130
156
|
}
|
|
157
|
+
if (a.optionId === 'reroll' && a.register) {
|
|
158
|
+
console.log(`REGISTER: the user steered the next hand to the ${a.register} register. Re-run concept-seed with the same key, the next --reroll round, and --register ${a.register}, then follow what it prints; the register is the user's steering, never yours to pre-select.`);
|
|
159
|
+
}
|
|
160
|
+
if (a.followup && a.optionId !== 'reroll') {
|
|
161
|
+
console.log('FOLLOWUP OPEN: the table stays open and the page is showing a loading hand. Deliver the next round now with --update --key <key> --payload <file>, then collect it with --wait; never leave the page waiting on a round you have not sent.');
|
|
162
|
+
}
|
|
163
|
+
if (a.buildPath === 'comp' || a.buildPath === 'code') {
|
|
164
|
+
// The page never writes the flip itself, but "never write it" overstated
|
|
165
|
+
// that into a rule the agent then applied to new-work's one-time offer,
|
|
166
|
+
// which exists for exactly this case: a flip on a project that had no
|
|
167
|
+
// recorded default is the only moment the preference is ever asked for.
|
|
168
|
+
const origin = a.buildPathFlipped
|
|
169
|
+
? 'flipped on the page, so it binds this session only, and the page never writes it back; the sole exception is new-work’s one-time offer, on a project that had no recorded default at all, which asks after the round closes and writes the answer to .impeccable/config.json'
|
|
170
|
+
: 'the round’s recorded default';
|
|
171
|
+
console.log(`BUILD PATH: ${a.buildPath} (${origin}). ${a.buildPath === 'comp'
|
|
172
|
+
? 'Comp-led: the chosen card’s comp is law; generate it before building when it does not exist yet, and the finish review audits the build against it.'
|
|
173
|
+
: 'Code-led: no comp is owed; a comp that already rendered rides at the finish review as the critique reference, and the ambition lives in the direction contract.'}`);
|
|
174
|
+
}
|
|
131
175
|
} catch { /* raw answer */ }
|
|
132
176
|
}
|
|
133
177
|
|
|
@@ -137,21 +181,29 @@ const portArg = Number(arg('port', '0'));
|
|
|
137
181
|
const QUESTION_DIR = path.join(process.cwd(), '.impeccable', 'questions');
|
|
138
182
|
const stateFile = (key) => path.join(QUESTION_DIR, `${key}.state.json`);
|
|
139
183
|
const answerFile = (key) => path.join(QUESTION_DIR, `${key}.answer.json`);
|
|
184
|
+
// A code-to-comp flip mid-round: the page records it here and --wait
|
|
185
|
+
// surfaces it as its own event, because the agent must start generating
|
|
186
|
+
// comps while the round is still open. Comp-to-code needs no event; it is
|
|
187
|
+
// free and rides the final ANSWER.
|
|
188
|
+
const flipFile = (key) => path.join(QUESTION_DIR, `${key}.flip.json`);
|
|
140
189
|
|
|
141
190
|
if (hasFlag('schema')) {
|
|
142
191
|
console.log(JSON.stringify({
|
|
143
192
|
title: 'Choose the visual world',
|
|
144
193
|
question: 'The roll assigned Fillmore Handbill. Keep it, take an alternate, or re-roll.',
|
|
145
194
|
options: [
|
|
146
|
-
{ id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.',
|
|
147
|
-
{ id: '
|
|
195
|
+
{ id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], comp: '.impeccable/mocks/decision/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' },
|
|
196
|
+
{ id: 'model-pick', label: 'The Broadside Ballad', kicker: 'IMPECCABLE’S PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the day’s ballad sheet.', palette: ['#1f1c18', '#efe5d0', '#a33327'], materials: ['woodcut', 'rag paper'], viewport: 'One tall sheet, the newest release as today’s ballad.', risk: 'Also the direction most runs in this category land on.', comp: '.impeccable/mocks/decision/model-pick.webp' },
|
|
197
|
+
{ id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', palette: ['#0000c0', '#ffff00', '#00c000', '#ffffff'], materials: ['block mosaic', 'phosphor glow'], viewport: 'P100 index page, releases as numbered rows.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', risk: 'Reads retro-novelty when the grid is not strict.', comp: '.impeccable/mocks/decision/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' },
|
|
198
|
+
{ id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', palette: ['#101418', '#9fb4c0'], materials: ['film grain', 'backlit glass'], case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' },
|
|
148
199
|
],
|
|
149
|
-
reroll:
|
|
200
|
+
reroll: { registers: ['safer', 'bolder'] },
|
|
201
|
+
buildPath: { value: 'comp', toggle: true },
|
|
150
202
|
canon: true,
|
|
151
|
-
canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', viewport: 'The arrangement a visitor expects, at full craft.',
|
|
203
|
+
canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', palette: ['#ffffff', '#111827', '#2563eb'], materials: ['clean grid', 'product photography'], viewport: 'The arrangement a visitor expects, at full craft.', risk: 'Indistinguishable from the competition by design.', comp: '.impeccable/mocks/decision/canon.webp' },
|
|
152
204
|
steer: true,
|
|
153
205
|
}, null, 2));
|
|
154
|
-
console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/
|
|
206
|
+
console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. A card may instead declare "wireframe" ({"cols":12,"rows":10,"regions":[{"label":"nav rail","x":0,"y":0,"w":3,"h":10,"accent":true}]}): the page draws it as a layout schematic in the media slot; surface-scope rounds use it on code-led builds, it never counts toward salience, and the card keeps its full read on the front. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register <value> for the next round; offer the registers on direction rounds, and never pre-select one. buildPath rides the payload as { "value": "comp"|"code", "toggle": true }: the value is the recorded default (.impeccable/config.json buildPath, or .impeccable/config.local.json where one machine differs) and the toggle renders a footer switch whose flip binds that session only; the ANSWER then carries buildPath plus buildPathFlipped. On a code-led round each card still declares its comp path as a flip reserve: wireframes render, and a flip to comp makes --wait return once with BUILD PATH FLIPPED so you generate the comps into the declared slots while the round stays open; a flip back to code is free, and a comp that already landed stays as the critique reference. The toggle may only be offered when image generation exists: a harness with no image tool and no API key never sets toggle: true, so the choice never renders where comps cannot be made, and code-led simply rides as the untoggleable value. followup: true keeps the table open after a pick for a second round via --update; send the next payload immediately, the page is waiting on it.');
|
|
155
207
|
process.exit(0);
|
|
156
208
|
}
|
|
157
209
|
|
|
@@ -161,15 +213,32 @@ if (hasFlag('wait')) {
|
|
|
161
213
|
const pollSec = Number(arg('poll', '60'));
|
|
162
214
|
const deadline = Date.now() + pollSec * 1000;
|
|
163
215
|
const answered = () => fs.existsSync(answerFile(key));
|
|
216
|
+
// Liveness must survive sandboxes: a sandboxed --wait cannot signal the
|
|
217
|
+
// daemon (kill throws EPERM even for a living process), so a fresh page
|
|
218
|
+
// heartbeat in the state file is the primary proof of life, the kill probe
|
|
219
|
+
// is secondary, and EPERM specifically means "exists, but the sandbox
|
|
220
|
+
// blocks signals", never "dead". Treating EPERM as death told one session
|
|
221
|
+
// the user had walked away while they were still reading the board.
|
|
164
222
|
const alive = () => {
|
|
165
|
-
try {
|
|
166
|
-
|
|
223
|
+
try {
|
|
224
|
+
const state = JSON.parse(fs.readFileSync(stateFile(key), 'utf8'));
|
|
225
|
+
if (state.lastBeat && Date.now() - state.lastBeat < 12000) return true;
|
|
226
|
+
try { process.kill(state.pid, 0); return true; }
|
|
227
|
+
catch (err) { return err.code === 'EPERM'; }
|
|
228
|
+
} catch { return false; }
|
|
167
229
|
};
|
|
168
230
|
let sawClose = false;
|
|
169
231
|
while (Date.now() < deadline) {
|
|
170
232
|
if (answered()) break;
|
|
233
|
+
// A build-path flip is its own event, not an answer: the round stays
|
|
234
|
+
// open, and the agent's job right now is comps, not code.
|
|
235
|
+
if (fs.existsSync(flipFile(key))) {
|
|
236
|
+
try { fs.rmSync(flipFile(key)); } catch { /* consumed elsewhere */ }
|
|
237
|
+
console.log('BUILD PATH FLIPPED: comp (for this session only; never write it to settings). The table is still open and the page shows shimmer where the images will land: generate each open card’s comp into its declared path now, lead first, then collect the answer with --wait again. A card whose comp already exists needs nothing.');
|
|
238
|
+
process.exit(0);
|
|
239
|
+
}
|
|
171
240
|
if (!alive()) {
|
|
172
|
-
console.log('serve-question: the question server is gone with no answer');
|
|
241
|
+
console.log('serve-question: the question server is gone with no answer. This is a server failure, not a user decision: restart it with --start and the same payload, reopen the URL for the user, and wait again. Never proceed without their choice while their browser session is open.');
|
|
173
242
|
process.exit(2);
|
|
174
243
|
}
|
|
175
244
|
try {
|
|
@@ -185,12 +254,16 @@ if (hasFlag('wait')) {
|
|
|
185
254
|
if (!answered()) { console.log(`WAITING: no answer yet after ${pollSec}s; run --wait --key ${key} again`); process.exit(3); }
|
|
186
255
|
const collected = fs.readFileSync(answerFile(key), 'utf8').trim();
|
|
187
256
|
printAnswer(collected);
|
|
188
|
-
// A re-roll keeps the table open: the server stays
|
|
189
|
-
// so only the answer file is consumed. Terminal
|
|
190
|
-
|
|
191
|
-
|
|
257
|
+
// A re-roll or a followup-round pick keeps the table open: the server stays
|
|
258
|
+
// alive awaiting --update, so only the answer file is consumed. Terminal
|
|
259
|
+
// choices clean up fully.
|
|
260
|
+
let keepsTableOpen = false;
|
|
261
|
+
try {
|
|
262
|
+
const parsedAnswer = JSON.parse(collected);
|
|
263
|
+
keepsTableOpen = parsedAnswer.optionId === 'reroll' || parsedAnswer.followup === true;
|
|
264
|
+
} catch { /* treat as terminal */ }
|
|
192
265
|
try { fs.rmSync(answerFile(key)); } catch { /* already gone */ }
|
|
193
|
-
if (!
|
|
266
|
+
if (!keepsTableOpen) { try { fs.rmSync(stateFile(key)); } catch { /* already gone */ } }
|
|
194
267
|
process.exit(0);
|
|
195
268
|
}
|
|
196
269
|
|
|
@@ -259,6 +332,12 @@ else raw = fs.readFileSync(0, 'utf8');
|
|
|
259
332
|
let payload;
|
|
260
333
|
let options;
|
|
261
334
|
let localImages = [];
|
|
335
|
+
// Build path (comp-led vs code-led): the payload carries the recorded
|
|
336
|
+
// default; the page's toggle updates the live value per session. The server
|
|
337
|
+
// owns both so the final ANSWER states the path and whether it was flipped
|
|
338
|
+
// even when the round never rendered a toggle.
|
|
339
|
+
let buildPathDefault = null;
|
|
340
|
+
let liveBuildPath = null;
|
|
262
341
|
|
|
263
342
|
function loadRound(json) {
|
|
264
343
|
const parsed = JSON.parse(json);
|
|
@@ -274,10 +353,10 @@ function loadRound(json) {
|
|
|
274
353
|
localImages.push(abs);
|
|
275
354
|
return `/img/${localImages.length - 1}`;
|
|
276
355
|
};
|
|
277
|
-
//
|
|
356
|
+
// Comps stream in after the page is served, so their slots register
|
|
278
357
|
// whether or not the file exists yet; /img answers 404 until it lands and
|
|
279
|
-
// the page polls the slot. Remote
|
|
280
|
-
const
|
|
358
|
+
// the page polls the slot. Remote comp URLs pass through untouched.
|
|
359
|
+
const compSrc = (value) => {
|
|
281
360
|
if (!value) return null;
|
|
282
361
|
if (/^https?:\/\//.test(value)) return value;
|
|
283
362
|
localImages.push(path.resolve(value));
|
|
@@ -288,14 +367,26 @@ function loadRound(json) {
|
|
|
288
367
|
...option,
|
|
289
368
|
heroSrc: imageSrc(option.hero),
|
|
290
369
|
boardSrc: imageSrc(option.board),
|
|
291
|
-
|
|
370
|
+
compSrc: compSrc(option.comp ?? option.sketch),
|
|
292
371
|
});
|
|
293
372
|
options = parsed.options.map(decorate);
|
|
373
|
+
// The verdict routes rendering: full cards first, then the canon, then the
|
|
374
|
+
// declined cards dead last in their own payload order. The reorder happens
|
|
375
|
+
// here so a payload that interleaves them still renders the weighing's
|
|
376
|
+
// shape, and the deck reads as a gradient of standing: contenders, the
|
|
377
|
+
// familiar door, then the demoted row.
|
|
378
|
+
const declined = options.filter((o) => o.verdict === 'declined');
|
|
379
|
+
options = options.filter((o) => o.verdict !== 'declined');
|
|
294
380
|
// The standing exit as a full card: same anatomy, reserved id, rendered
|
|
295
381
|
// subordinate by the page. Without it, canon stays the quiet footer action.
|
|
296
382
|
if (parsed.canonCard && typeof parsed.canonCard === 'object') {
|
|
297
383
|
options = [...options, { ...decorate(parsed.canonCard), id: 'canon', isCanon: true }];
|
|
298
384
|
}
|
|
385
|
+
options = [...options, ...declined];
|
|
386
|
+
buildPathDefault = (parsed.buildPath && (parsed.buildPath.value === 'comp' || parsed.buildPath.value === 'code'))
|
|
387
|
+
? { value: parsed.buildPath.value, toggle: parsed.buildPath.toggle === true }
|
|
388
|
+
: null;
|
|
389
|
+
liveBuildPath = buildPathDefault?.value ?? null;
|
|
299
390
|
}
|
|
300
391
|
try { loadRound(raw); } catch (error) { console.error(`serve-question: ${error.message}`); process.exit(1); }
|
|
301
392
|
const detachedKey = hasFlag('detached-serve') ? arg('key') : null;
|
|
@@ -311,7 +402,30 @@ function page() {
|
|
|
311
402
|
// and material tags give a text-only direction an immediate identity that
|
|
312
403
|
// no generation luck can distort.
|
|
313
404
|
const fact = (label, value, cls = '') => value ? `<p class="fact${cls ? ` ${cls}` : ''}"><span class="fact-label">${label}</span>${esc(value)}</p>` : '';
|
|
314
|
-
const
|
|
405
|
+
const demoted = (option) => option.verdict === 'declined';
|
|
406
|
+
// The build path (comp-led vs code-led) is a workflow preference, not a
|
|
407
|
+
// design decision: the payload carries the recorded default and whether
|
|
408
|
+
// the page offers the toggle. On a code-led round a declared comp path is
|
|
409
|
+
// a flip reserve, not a face: wireframes render, and the slot only starts
|
|
410
|
+
// shimmering when the user flips to comp.
|
|
411
|
+
const buildPath = buildPathDefault;
|
|
412
|
+
const codeLed = buildPath?.value === 'code';
|
|
413
|
+
// Salience parity: a card's imagery weight is capped by the assigned card's.
|
|
414
|
+
// When the lead card has no media at all (no image generation this round,
|
|
415
|
+
// and no catalog art of its own), full-bleed catalog art beside a text-only
|
|
416
|
+
// assigned card would let rendering luck outvote the weighing: users click
|
|
417
|
+
// the colorful thing. Declined cards are thumb-only regardless; the verdict
|
|
418
|
+
// demoted them, and a full-bleed hero would promote them right back.
|
|
419
|
+
const identityRound = !(options[0] && (options[0].compSrc || options[0].heroSrc || options[0].boardSrc));
|
|
420
|
+
// A declined card never renders a full media face, comp included: even a
|
|
421
|
+
// declared comp would buy back the salience the verdict took away.
|
|
422
|
+
const faceComp = (option) => (demoted(option) || codeLed) ? null : option.compSrc;
|
|
423
|
+
const thumbOnly = (option) => !faceComp(option) && Boolean(option.heroSrc || option.boardSrc) && (demoted(option) || identityRound);
|
|
424
|
+
const hasMedia = (option) => Boolean(faceComp(option) || ((option.heroSrc || option.boardSrc) && !thumbOnly(option)));
|
|
425
|
+
// The back exists to keep long facts off a card whose front is an image;
|
|
426
|
+
// a card with no art has no flip chip to reach it, so it gets no back and
|
|
427
|
+
// the full read lives on the front instead.
|
|
428
|
+
const hasBack = (option) => hasMedia(option) && Boolean(option.viewport || option.case || (option.boardSrc && option.heroSrc));
|
|
315
429
|
const anatomy = (option) => {
|
|
316
430
|
const rows = [];
|
|
317
431
|
if (option.thesis) rows.push(`<p class="thesis">${esc(option.thesis)}</p>`);
|
|
@@ -323,10 +437,44 @@ function page() {
|
|
|
323
437
|
idBits.push(option.materials.slice(0, 4).map((m) => `<span class="tag">${esc(m)}</span>`).join(''));
|
|
324
438
|
}
|
|
325
439
|
if (idBits.length) rows.push(`<div class="identity">${idBits.join('')}</div>`);
|
|
440
|
+
// Donations from declined challengers render as named raise lines: the
|
|
441
|
+
// assigned card arrives already raised by the hand it beat, and the raise
|
|
442
|
+
// is readable, because a raise nobody can read did not happen. One raise
|
|
443
|
+
// renders inline; several become a compact cycler (click advances), so a
|
|
444
|
+
// generous hand cannot blow the card out of proportion.
|
|
445
|
+
if (Array.isArray(option.raised) && option.raised.length) {
|
|
446
|
+
const nameOf = (id) => options.find((o) => o.id === id)?.label || String(id ?? '');
|
|
447
|
+
const raiseLines = option.raised.slice(0, 6).map((r) => `<p class="raise"><span class="fact-label">From ${esc(nameOf(r.from))}</span>${esc(r.raise || r.kept || '')}</p>`);
|
|
448
|
+
const raisesHead = (count) => `<div class="raises-head"><span class="fact-label">Improved by Impeccable's worlds</span>${count > 1 ? `<span class="raises-count" data-raises-count>1/${count}</span>` : ''}</div>`;
|
|
449
|
+
if (raiseLines.length > 1) {
|
|
450
|
+
rows.push(`<div class="raises raises-cycle" role="button" tabindex="0" title="Click or press Enter for the next improvement" aria-label="How Impeccable's worlds improved this direction; activate to see the next improvement">
|
|
451
|
+
${raisesHead(raiseLines.length)}
|
|
452
|
+
${raiseLines.join('')}
|
|
453
|
+
<span class="sr-live" aria-live="polite"></span>
|
|
454
|
+
</div>`);
|
|
455
|
+
} else {
|
|
456
|
+
rows.push(`<div class="raises">${raisesHead(1)}${raiseLines[0]}</div>`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
// Demoted art stays reachable as a labeled thumb: the catalog world
|
|
460
|
+
// explains where the direction comes from without buying it back the
|
|
461
|
+
// salience the verdict took away.
|
|
462
|
+
if (thumbOnly(option)) {
|
|
463
|
+
rows.push(`<figure class="inspo" title="Inspiration: the world this direction draws from. Your page will not look like this image."><img src="${esc(option.heroSrc || option.boardSrc)}" alt=""><figcaption>inspired by</figcaption></figure>`);
|
|
464
|
+
}
|
|
326
465
|
// The front carries only what the choice needs: thesis, identity, and the
|
|
327
466
|
// honest risk clamped to two lines. First viewport and the case read on
|
|
328
|
-
// the card's back; once the
|
|
329
|
-
|
|
467
|
+
// the card's back; once the comp lands, the first viewport is a picture.
|
|
468
|
+
// With no art there is no back, so the full read fills the room the
|
|
469
|
+
// image would have taken.
|
|
470
|
+
if (hasMedia(option)) {
|
|
471
|
+
rows.push(fact('Risk', option.risk, 'clamp'));
|
|
472
|
+
} else {
|
|
473
|
+
rows.push(fact('First viewport', option.viewport));
|
|
474
|
+
rows.push(fact('The case', option.case));
|
|
475
|
+
rows.push(fact('Kept', option.kept));
|
|
476
|
+
rows.push(fact('Risk', option.risk));
|
|
477
|
+
}
|
|
330
478
|
if (!option.thesis && option.body) rows.push(`<p class="detail">${esc(option.body)}</p>`);
|
|
331
479
|
else if (option.body && option.thesis && !hasBack(option)) rows.push(`<p class="detail more">${esc(option.body)}</p>`);
|
|
332
480
|
return rows.join('\n ');
|
|
@@ -334,42 +482,76 @@ function page() {
|
|
|
334
482
|
const backFacts = (option) => [
|
|
335
483
|
fact('First viewport', option.viewport),
|
|
336
484
|
fact('The case', option.case),
|
|
485
|
+
fact('Kept', option.kept),
|
|
337
486
|
fact('Risk', option.risk),
|
|
338
487
|
option.body && option.thesis ? `<p class="detail more">${esc(option.body)}</p>` : '',
|
|
339
488
|
].filter(Boolean).join('\n ');
|
|
340
489
|
const media = (option) => {
|
|
341
|
-
const
|
|
342
|
-
|
|
490
|
+
const inspirationSrc = option.heroSrc || option.boardSrc;
|
|
491
|
+
const inspiration = inspirationSrc ? `<figure class="pip" title="Inspiration: the world this direction draws from. Your page will not look like this image.">
|
|
492
|
+
<img src="${esc(inspirationSrc)}" alt="">
|
|
343
493
|
<figcaption>inspiration</figcaption>
|
|
344
494
|
</figure>` : '';
|
|
345
495
|
const details = hasBack(option) ? flipChip('Details') : '';
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
496
|
+
// Thumb-only art renders inside the body via anatomy(), never as a face,
|
|
497
|
+
// and a declined card's comp slot is ignored outright.
|
|
498
|
+
if (thumbOnly(option)) return '';
|
|
499
|
+
if (faceComp(option)) {
|
|
500
|
+
const textOnlyFacts = backFacts(option);
|
|
501
|
+
return `<div class="media comp-pending" data-comp="${esc(option.compSrc)}">
|
|
502
|
+
<div class="shimmer"><span class="comp-note">rendering…</span></div>
|
|
503
|
+
<img class="comp" alt="" hidden>
|
|
350
504
|
${inspiration}
|
|
505
|
+
<template class="text-only-facts">${textOnlyFacts}</template>
|
|
351
506
|
<div class="chips">${expandChip}${details}</div>
|
|
352
507
|
</div>`;
|
|
353
508
|
}
|
|
354
509
|
if (option.heroSrc || option.boardSrc) {
|
|
355
|
-
|
|
510
|
+
// Without a comp the catalog art is the card's face; it stays a
|
|
511
|
+
// labeled reference so it never reads as the promise of the build.
|
|
512
|
+
return `<div class="media" title="Inspiration: the world this direction draws from. Your page will not look like this image.">
|
|
356
513
|
<img src="${esc(option.heroSrc || option.boardSrc)}" alt="">
|
|
514
|
+
<p class="media-label">inspiration</p>
|
|
357
515
|
<div class="chips">${expandChip}${details}</div>
|
|
358
516
|
</div>`;
|
|
359
517
|
}
|
|
360
518
|
return '';
|
|
361
519
|
};
|
|
520
|
+
// Wireframe media: a code-led card's layout schematic, authored as grid
|
|
521
|
+
// regions in the payload and drawn by the page; boxes and labels, no art.
|
|
522
|
+
// It fills the media slot only when the card has no imagery, and it never
|
|
523
|
+
// counts toward salience or earns a card back: the full read stays on the
|
|
524
|
+
// front, exactly like a text-only card.
|
|
525
|
+
const wire = (option) => {
|
|
526
|
+
const frame = option.wireframe;
|
|
527
|
+
if (!frame || !Array.isArray(frame.regions) || !frame.regions.length || media(option) || demoted(option)) return '';
|
|
528
|
+
const cols = Number(frame.cols) > 0 ? Number(frame.cols) : 12;
|
|
529
|
+
const rows = Number(frame.rows) > 0 ? Number(frame.rows) : 10;
|
|
530
|
+
const pct = (n, total) => `${Math.max(0, Math.min(100, (n / total) * 100)).toFixed(2)}%`;
|
|
531
|
+
const cells = frame.regions.slice(0, 12).map((region) => {
|
|
532
|
+
const x = Number(region.x) || 0;
|
|
533
|
+
const y = Number(region.y) || 0;
|
|
534
|
+
const w = Math.max(Number(region.w) || 1, 0.5);
|
|
535
|
+
const h = Math.max(Number(region.h) || 1, 0.5);
|
|
536
|
+
return `<div class="wire-region${region.accent ? ' accent' : ''}" style="left:${pct(x, cols)};top:${pct(y, rows)};width:${pct(w, cols)};height:${pct(h, rows)}"><span>${esc(region.label || '')}</span></div>`;
|
|
537
|
+
}).join('');
|
|
538
|
+
return `<div class="media wire" role="img" aria-label="Layout schematic">
|
|
539
|
+
<div class="wire-field">${cells}</div>
|
|
540
|
+
<p class="media-label">layout</p>
|
|
541
|
+
</div>`;
|
|
542
|
+
};
|
|
543
|
+
const chooseLabel = (option) => option.isCanon ? 'Play it straight' : demoted(option) ? 'Adopt anyway' : 'Build this';
|
|
362
544
|
const cards = options.map((option, index) => `
|
|
363
|
-
<article class="card${option.isCanon ? ' canon' : ''}" style="--fan:${index === 0 ? '0deg' : (index % 2 ? '1.4deg' : '-1.2deg')};--deal:${index * 90}ms" data-id="${esc(option.id)}">
|
|
545
|
+
<article class="card${option.isCanon ? ' canon' : ''}${demoted(option) ? ' declined' : ''}" style="--fan:${index === 0 ? '0deg' : (index % 2 ? '1.4deg' : '-1.2deg')};--deal:${index * 90}ms" data-id="${esc(option.id)}"${codeLed && option.compSrc && !demoted(option) ? ` data-comp-slot="${esc(option.compSrc)}"` : ''}>
|
|
364
546
|
<div class="card-inner">
|
|
365
|
-
<div class="face front${index === 0 ? ' lead' : ''}${media(option) ? '' : ' text-only'}">
|
|
366
|
-
${option.kicker ? `<span class="kicker">${esc(option.kicker)}</span>` : option.isCanon ? '<span class="kicker standing">The standing door</span>' : ''}
|
|
367
|
-
${media(option)}
|
|
547
|
+
<div class="face front${index === 0 ? ' lead' : ''}${(media(option) || wire(option)) ? '' : ' text-only'}">
|
|
548
|
+
${option.kicker ? `<span class="kicker">${esc(option.kicker)}</span>` : demoted(option) ? '<span class="kicker declined-k">Declined</span>' : option.isCanon ? '<span class="kicker standing">The standing door</span>' : ''}
|
|
549
|
+
${media(option) || wire(option)}
|
|
368
550
|
<div class="body">
|
|
369
551
|
${option.lineage ? `<p class="tier">${esc(option.lineage)}</p>` : ''}
|
|
370
552
|
<h2>${esc(option.label)}</h2>
|
|
371
553
|
${anatomy(option)}
|
|
372
|
-
<button class="choose" data-id="${esc(option.id)}">${option
|
|
554
|
+
<button class="choose" data-id="${esc(option.id)}">${chooseLabel(option)}</button>
|
|
373
555
|
</div>
|
|
374
556
|
</div>
|
|
375
557
|
${hasBack(option) ? `<div class="face back${index === 0 ? ' lead' : ''}">
|
|
@@ -380,7 +562,7 @@ function page() {
|
|
|
380
562
|
<div class="body back-body">
|
|
381
563
|
${option.boardSrc ? `<p class="tier">The full read · ${esc(option.label)}</p>` : ''}
|
|
382
564
|
${backFacts(option)}
|
|
383
|
-
<button class="choose" data-id="${esc(option.id)}">${option
|
|
565
|
+
<button class="choose" data-id="${esc(option.id)}">${chooseLabel(option)}</button>
|
|
384
566
|
</div>
|
|
385
567
|
</div>` : ''}
|
|
386
568
|
</div>
|
|
@@ -413,9 +595,12 @@ function page() {
|
|
|
413
595
|
--ks-font-display: "Alumni Sans", "Albert Sans", Arial, sans-serif;
|
|
414
596
|
--ks-font: "Albert Sans", "Avenir Next", "Helvetica Neue", Arial, system-ui, sans-serif;
|
|
415
597
|
--ks-mono: "SFMono-Regular", "Roboto Mono", "JetBrains Mono", Consolas, monospace;
|
|
598
|
+
/* One inset shared by the content column, the deck's snap padding, and
|
|
599
|
+
the sticky footer, so all three align on the same 90rem column. */
|
|
600
|
+
--page-inset: max(clamp(1rem, 5vw, 4rem), calc((100vw - 90rem) / 2));
|
|
416
601
|
}
|
|
417
602
|
* { box-sizing: border-box; margin: 0; }
|
|
418
|
-
body { background: var(--ks-lacquer); color: var(--ks-text); font: 15px/1.55 var(--ks-font); padding: 1.8rem clamp(1rem, 5vw, 4rem)
|
|
603
|
+
body { background: var(--ks-lacquer); color: var(--ks-text); font: 15px/1.55 var(--ks-font); padding: 1.8rem clamp(1rem, 5vw, 4rem) 0; min-height: 100dvh; display: flex; flex-direction: column; overflow-x: clip; }
|
|
419
604
|
#ambient { position: fixed; inset: -40px; z-index: 0; background-size: cover; background-position: center; filter: blur(34px) saturate(1.05); opacity: 0; transition: opacity .55s ease, background-image .2s; pointer-events: none; }
|
|
420
605
|
#scrim { position: fixed; inset: 0; z-index: 0; background: linear-gradient(180deg, oklch(7% 0.006 95 / 0.62), oklch(7% 0.006 95 / 0.78)); pointer-events: none; }
|
|
421
606
|
header, main, footer { position: relative; z-index: 1; }
|
|
@@ -427,7 +612,7 @@ function page() {
|
|
|
427
612
|
.brand { display: flex; align-items: center; gap: .55rem; color: var(--ks-kinpaku); }
|
|
428
613
|
.brand svg { width: 22px; height: 22px; }
|
|
429
614
|
.wordmark { font-family: var(--ks-font-display); font-weight: 400; font-size: 1.125rem; letter-spacing: 0.15em; text-transform: uppercase; line-height: 1; color: var(--ks-kinpaku); }
|
|
430
|
-
.headline { display: flex; align-items: center; gap: .9rem; }
|
|
615
|
+
.headline { display: flex; align-items: center; gap: .9rem; flex-wrap: wrap; }
|
|
431
616
|
.headline-die { flex: none; width: 34px; height: 34px; color: var(--ks-kinpaku); }
|
|
432
617
|
h1 { font-family: var(--ks-font-display); font-weight: 100; font-size: clamp(2.6rem, 5vw, 4.2rem); letter-spacing: -0.01em; line-height: 1.02; color: var(--ks-champagne); }
|
|
433
618
|
.question { color: var(--ks-text-muted); margin-top: .7rem; max-width: 52rem; }
|
|
@@ -439,9 +624,24 @@ function page() {
|
|
|
439
624
|
.deck-shell { position: relative; width: 100vw; margin-left: calc(50% - 50vw); }
|
|
440
625
|
/* One row in a wide viewport, one column in a tall one; the deck scrolls on
|
|
441
626
|
its axis with snap points and the arrows page it card by card. */
|
|
442
|
-
.grid { --deck-inset:
|
|
627
|
+
.grid { --deck-inset: var(--page-inset); display: flex; gap: 1.6rem; width: 100%; overflow-x: auto; overflow-y: hidden; scroll-snap-type: x mandatory; scrollbar-width: none; padding: 6px var(--deck-inset); scroll-padding-inline: var(--deck-inset); align-items: stretch; }
|
|
443
628
|
.grid::-webkit-scrollbar { display: none; }
|
|
444
|
-
|
|
629
|
+
/* Wide enough that the comp carries the card: at 27vw the imagery read
|
|
630
|
+
as a thumbnail above a column of copy, and the copy won the attention
|
|
631
|
+
contest the comp is supposed to win. */
|
|
632
|
+
.grid > .card { flex: 0 0 clamp(24rem, 34vw, 34rem); scroll-snap-align: center; }
|
|
633
|
+
/* Short landscape viewports (13-inch laptops): header, a 34vw card, and the
|
|
634
|
+
footer do not fit 800px of height, so the headline compacts and the deck
|
|
635
|
+
narrows. Height is the axis that gives; the sticky footer keeps the
|
|
636
|
+
round's verbs on screen while a too-tall card scrolls. */
|
|
637
|
+
@media (min-aspect-ratio: 1/1) and (max-height: 900px) {
|
|
638
|
+
body { padding-top: 1.1rem; }
|
|
639
|
+
h1 { font-size: clamp(2rem, 3.4vw, 2.9rem); }
|
|
640
|
+
.question { margin-top: .45rem; }
|
|
641
|
+
.stage { gap: 1rem; }
|
|
642
|
+
.grid > .card { flex-basis: clamp(20rem, 27vw, 27rem); }
|
|
643
|
+
.grid > .card.declined { flex-basis: clamp(13rem, 18vw, 18rem); }
|
|
644
|
+
}
|
|
445
645
|
.nav { position: absolute; z-index: 6; width: 42px; height: 42px; display: flex; align-items: center; justify-content: center; border-radius: 50%; background: oklch(7% 0.006 95 / 0.78); border: 1px solid var(--ks-rule); color: var(--ks-kinpaku); cursor: pointer; backdrop-filter: blur(6px); transition: border-color .2s, color .2s, opacity .2s; }
|
|
446
646
|
.nav:hover { border-color: var(--ks-kinpaku-deep); color: var(--ks-kinpaku-pale); }
|
|
447
647
|
.nav[disabled] { opacity: .25; cursor: default; }
|
|
@@ -468,6 +668,14 @@ function page() {
|
|
|
468
668
|
.nav.next { right: auto; left: 50%; top: auto; bottom: 6px; transform: translate(-50%, 0); }
|
|
469
669
|
.fade-prev { top: 0; left: 0; right: 0; bottom: auto; width: auto; height: 72px; background: linear-gradient(180deg, var(--ks-lacquer), transparent); }
|
|
470
670
|
.fade-next { top: auto; left: 0; right: 0; bottom: 0; width: auto; height: 72px; background: linear-gradient(0deg, var(--ks-lacquer), transparent); }
|
|
671
|
+
/* In the vertical deck the cross axis is horizontal: flex-start would
|
|
672
|
+
shrink a declined card to content WIDTH, not height, so it stretches
|
|
673
|
+
like every other card and its height is already its own. */
|
|
674
|
+
.grid > .card.declined { align-self: stretch; }
|
|
675
|
+
/* The sticky bar is a wide-viewport fix. Here it would sit over the
|
|
676
|
+
deck's More pager and cost a third of a phone screen, and the deck
|
|
677
|
+
already scrolls internally, so the footer stays in the page flow. */
|
|
678
|
+
footer { position: static; width: auto; margin: 1rem 0 0; padding: .7rem 0 1.2rem; background: transparent; border-top: 0; backdrop-filter: none; }
|
|
471
679
|
}
|
|
472
680
|
.card { position: relative; perspective: 1400px; transform: rotate(var(--fan, 0deg)); transition: transform .25s cubic-bezier(.16, 1, .3, 1); }
|
|
473
681
|
.card:hover { transform: rotate(0deg) translateY(-4px); }
|
|
@@ -491,8 +699,13 @@ function page() {
|
|
|
491
699
|
region entirely instead of reserving a blank 16:9 void. */
|
|
492
700
|
.face.text-only .kicker { position: static; align-self: flex-start; margin: 14px 0 0 14px; }
|
|
493
701
|
.face.text-only .body { padding-top: 12px; }
|
|
494
|
-
|
|
702
|
+
/* 16/10 matches the landscape comp frame; portrait art overrides the
|
|
703
|
+
slot with its own exact ratio at load (see the load listener), and the
|
|
704
|
+
deck narrows so portrait cards line up side by side. */
|
|
705
|
+
.media { position: relative; width: 100%; aspect-ratio: 16/10; flex: none; }
|
|
706
|
+
.grid.portrait-media > .card { flex-basis: clamp(14rem, 19vw, 19rem); }
|
|
495
707
|
.media img { width: 100%; height: 100%; object-fit: cover; display: block; background: linear-gradient(100deg, var(--ks-graphite) 40%, var(--ks-graphite-2) 50%, var(--ks-graphite) 60%); }
|
|
708
|
+
.media > img:not([hidden]) { cursor: zoom-in; }
|
|
496
709
|
.face.back { background: var(--ks-lacquer-raised); }
|
|
497
710
|
.back-bar { margin-top: auto; background: var(--ks-lacquer-raised); }
|
|
498
711
|
.hero-blank { width: 100%; height: 100%; background: linear-gradient(100deg, var(--ks-graphite) 40%, var(--ks-graphite-2) 50%, var(--ks-graphite) 60%); }
|
|
@@ -522,24 +735,82 @@ function page() {
|
|
|
522
735
|
.body.back-body { overflow-y: auto; flex: 1; scrollbar-width: thin; }
|
|
523
736
|
/* Inspiration rides picture-in-picture: the catalog world explains where the
|
|
524
737
|
direction comes from without promising what the build will look like. */
|
|
525
|
-
/* Hovering the inspiration takes over the whole media region; the
|
|
738
|
+
/* Hovering the inspiration takes over the whole media region; the comp is
|
|
526
739
|
the promise, the inspiration is a glance, so the glance must cost nothing. */
|
|
527
740
|
.pip { position: absolute; z-index: 2; left: 10px; bottom: 10px; margin: 0; width: 84px; height: 64px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; background: var(--ks-lacquer); cursor: zoom-in; transition: left .35s cubic-bezier(.16,1,.3,1), bottom .35s cubic-bezier(.16,1,.3,1), width .35s cubic-bezier(.16,1,.3,1), height .35s cubic-bezier(.16,1,.3,1), border-radius .35s ease; box-shadow: 0 6px 18px oklch(0% 0 0 / 0.45); }
|
|
528
741
|
.pip img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
|
529
742
|
.pip figcaption { position: absolute; left: 0; right: 0; bottom: 0; font-family: var(--ks-mono); font-size: .5rem; letter-spacing: .2em; text-transform: uppercase; color: var(--ks-text); text-align: center; padding: 3px 0 4px; background: oklch(7% 0.006 95 / 0.72); backdrop-filter: blur(3px); }
|
|
530
743
|
.pip:hover { left: 0; bottom: 0; width: 100%; height: 100%; border-radius: 0; z-index: 3; }
|
|
531
|
-
.
|
|
744
|
+
.comp-note { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-family: var(--ks-mono); font-size: .66rem; letter-spacing: .22em; text-transform: uppercase; color: var(--ks-text-faint); }
|
|
745
|
+
/* Catalog art standing in for a comp-less card is a reference, and says so
|
|
746
|
+
on its face; the same pill later carries "artwork unavailable". */
|
|
747
|
+
.media-label { position: absolute; z-index: 2; left: 10px; bottom: 10px; margin: 0; font-family: var(--ks-mono); font-size: .5rem; letter-spacing: .2em; text-transform: uppercase; color: var(--ks-text); padding: 3px 8px 4px; background: oklch(7% 0.006 95 / 0.72); border: 1px solid var(--ks-rule); border-radius: 4px; backdrop-filter: blur(3px); }
|
|
748
|
+
/* Art that never arrives collapses to the card's own palette (painted
|
|
749
|
+
inline from its swatches) instead of sitting as a dark void wearing a
|
|
750
|
+
zoom cursor; the scrim keeps the label legible over saturated fields,
|
|
751
|
+
passes clicks through, and the flip chips stay above it. A card with no
|
|
752
|
+
palette falls back to the quiet graphite field. */
|
|
753
|
+
.media.unavailable { background: linear-gradient(100deg, var(--ks-graphite) 40%, var(--ks-graphite-2) 50%, var(--ks-graphite) 60%); }
|
|
754
|
+
.media.unavailable::after { content: ""; position: absolute; inset: 0; z-index: 1; background: oklch(10% 0.008 95 / 0.45); pointer-events: none; }
|
|
755
|
+
.media.unavailable .chips { z-index: 2; }
|
|
532
756
|
/* A stand-in is honest about being one: dimmed, labeled, and replaced by
|
|
533
|
-
the real
|
|
534
|
-
.media.stand-in img.
|
|
757
|
+
the real comp whenever it lands. */
|
|
758
|
+
.media.stand-in img.comp { filter: brightness(.72) saturate(.85); }
|
|
535
759
|
.media.stand-in .pip { display: none; }
|
|
536
760
|
.stand-in-label { position: absolute; z-index: 2; left: 0; right: 0; bottom: 0; margin: 0; font-family: var(--ks-mono); font-size: .56rem; letter-spacing: .2em; text-transform: uppercase; color: var(--ks-text); text-align: center; padding: 4px 0 5px; background: oklch(7% 0.006 95 / 0.78); backdrop-filter: blur(3px); }
|
|
537
|
-
.media.
|
|
538
|
-
.media.
|
|
539
|
-
.media img.
|
|
761
|
+
.media.comp-pending { position: relative; }
|
|
762
|
+
.media.comp-pending .shimmer { position: absolute; inset: 0; }
|
|
763
|
+
.media img.comp { position: relative; z-index: 1; }
|
|
540
764
|
/* The generic .media img display:block would defeat [hidden] and float an
|
|
541
|
-
empty block over the shimmer; an unloaded
|
|
765
|
+
empty block over the shimmer; an unloaded comp must truly not render. */
|
|
542
766
|
.media img[hidden] { display: none; }
|
|
767
|
+
/* Declined challengers: the weighing demoted them, so the card is narrower
|
|
768
|
+
and quieter, its catalog art rides as a labeled thumb in the body, and
|
|
769
|
+
the action reads "Adopt anyway". Adoptable, never deleted: the demoted
|
|
770
|
+
row is the hand's proof of judgment. */
|
|
771
|
+
/* Narrow AND short: without align-self the stretch default drags a thin
|
|
772
|
+
declined card to the tallest contender's height, a strange stilt of a
|
|
773
|
+
card beside the full hand. */
|
|
774
|
+
.grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); align-self: flex-start; }
|
|
775
|
+
.card.declined .face { background: var(--ks-graphite); }
|
|
776
|
+
.card.declined:hover .face { border-color: var(--ks-text-faint); }
|
|
777
|
+
.card.declined h2 { font-size: 1rem; color: var(--ks-text); }
|
|
778
|
+
.kicker.declined-k { background: transparent; border: 1px solid var(--ks-rule); color: var(--ks-text-faint); }
|
|
779
|
+
.card.declined button.choose { background: transparent; color: var(--ks-text-muted); border: 1px solid var(--ks-rule); font-size: .85rem; padding: 8px 22px; }
|
|
780
|
+
.card.declined button.choose:hover { background: var(--ks-graphite-2); border-color: var(--ks-text-muted); }
|
|
781
|
+
/* Wireframe media: the code-led schematic. Quiet boxes in the card's own
|
|
782
|
+
chrome; uniform salience across cards by construction, so it needs no
|
|
783
|
+
parity rules. */
|
|
784
|
+
.media.wire { background: var(--ks-lacquer); border-bottom: 1px solid var(--ks-rule); }
|
|
785
|
+
.wire-field { position: absolute; inset: 12px 12px 26px; }
|
|
786
|
+
.wire-region { position: absolute; border: 1px solid oklch(78% 0 0 / 0.26); border-radius: 3px; background: oklch(78% 0 0 / 0.05); display: flex; align-items: center; justify-content: center; overflow: hidden; }
|
|
787
|
+
.wire-region span { font-family: var(--ks-mono); font-size: .55rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ks-text-faint); text-align: center; padding: 2px 4px; }
|
|
788
|
+
.wire-region.accent { border-color: oklch(84% 0.19 80.46 / 0.5); background: oklch(84% 0.19 80.46 / 0.06); }
|
|
789
|
+
.wire-region.accent span { color: var(--ks-kinpaku-rich); }
|
|
790
|
+
/* Thumb-scale inspiration: present, labeled, zoomable, and incapable of
|
|
791
|
+
outshouting a text-only assigned card. */
|
|
792
|
+
.inspo { position: relative; flex: none; margin: 2px 0; width: 104px; height: 64px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; cursor: zoom-in; background: var(--ks-lacquer); }
|
|
793
|
+
.inspo img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
|
794
|
+
.inspo figcaption { position: absolute; left: 0; right: 0; bottom: 0; font-family: var(--ks-mono); font-size: .48rem; letter-spacing: .16em; text-transform: uppercase; color: var(--ks-text); text-align: center; padding: 2px 0 3px; background: oklch(7% 0.006 95 / 0.72); }
|
|
795
|
+
/* Raises: the improvements the dealt worlds donated to the assigned
|
|
796
|
+
direction, each named for its donor world. Patina, not kinpaku:
|
|
797
|
+
provenance, not a call to action. A quiet contained panel, never an
|
|
798
|
+
accent side-tab. */
|
|
799
|
+
.raises { display: flex; flex-direction: column; gap: 4px; margin: 2px 0; padding: 7px 10px 8px; background: oklch(70% 0.12 188 / 0.06); border: 1px solid oklch(70% 0.12 188 / 0.22); border-radius: 8px; }
|
|
800
|
+
.raise { font-size: .78rem; color: var(--ks-text-muted); line-height: 1.45; }
|
|
801
|
+
.raise .fact-label { color: var(--ks-patina); }
|
|
802
|
+
/* Several kept ideas cycle instead of stacking: one visible at a time, a
|
|
803
|
+
counter for the rest, the whole block advances on click. */
|
|
804
|
+
.raises-cycle { cursor: pointer; transition: border-color .2s ease; }
|
|
805
|
+
.raises-cycle:hover { border-color: oklch(70% 0.12 188 / 0.45); }
|
|
806
|
+
.raises-cycle .raise { display: none; }
|
|
807
|
+
.raises-cycle .raise.active { display: block; }
|
|
808
|
+
.raises-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
|
|
809
|
+
.raises-head .fact-label { color: var(--ks-patina); }
|
|
810
|
+
.raises-count { font-family: var(--ks-mono); font-size: .58rem; letter-spacing: .14em; color: var(--ks-text-faint); }
|
|
811
|
+
.raises-count::after { content: " \\203A"; }
|
|
812
|
+
.raises-cycle:hover .raises-count { color: var(--ks-patina); }
|
|
813
|
+
.sr-live { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
|
|
543
814
|
/* The standing exit as a card: present with full anatomy, never dressed as a
|
|
544
815
|
contender. Graphite instead of kinpaku, and it never takes the lead ring. */
|
|
545
816
|
.card.canon .face { border-color: var(--ks-rule); background: var(--ks-graphite); }
|
|
@@ -549,12 +820,47 @@ function page() {
|
|
|
549
820
|
.card.canon button.choose:hover { border-color: var(--ks-text-muted); background: var(--ks-graphite-2); }
|
|
550
821
|
button.choose { margin-top: auto; align-self: start; background: var(--ks-kinpaku); color: var(--ks-dark-ink); border: 0; font-family: var(--ks-font); font-size: 1rem; font-weight: 500; line-height: 1.35; padding: 10px 38px; border-radius: 6px; cursor: pointer; transition: background .15s; }
|
|
551
822
|
button.choose:hover { background: var(--ks-kinpaku-pale); }
|
|
552
|
-
|
|
823
|
+
/* The round's verbs stay reachable on short viewports: the footer is a
|
|
824
|
+
full-bleed bar stuck to the viewport bottom and the deck scrolls under
|
|
825
|
+
it. Same inset as the content column, so the controls stay aligned. */
|
|
826
|
+
footer { position: sticky; bottom: 0; z-index: 10; width: 100vw; margin: 1.2rem calc(50% - 50vw) 0; padding: .7rem var(--page-inset) calc(.7rem + env(safe-area-inset-bottom, 0px)); display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; background: oklch(7% 0.006 95 / 0.82); backdrop-filter: blur(10px); border-top: 1px solid var(--ks-rule); }
|
|
553
827
|
#steer { flex: 1; min-width: 16rem; background: var(--ks-lacquer-raised); color: var(--ks-text); border: 1px solid var(--ks-rule); border-radius: 7px; padding: .6rem .85rem; font: inherit; }
|
|
554
828
|
#steer:focus { outline: none; border-color: var(--ks-patina); }
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
829
|
+
/* Build-path toggle: a workflow preference surfaced as a quiet segmented
|
|
830
|
+
control on the headline row, right-aligned opposite the title, its trade stated in
|
|
831
|
+
one line that changes with the selection. The default comes from the
|
|
832
|
+
payload (settings); flipping binds this session only, and the agent
|
|
833
|
+
learns about a code-to-comp flip live. Rendered only when the payload
|
|
834
|
+
offers it, which the agent does only when image generation exists. */
|
|
835
|
+
#build-path { display: flex; flex-direction: column; gap: 4px; align-items: flex-end; flex: none; margin-left: auto; }
|
|
836
|
+
.bp-switch { display: inline-flex; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; }
|
|
837
|
+
.bp-note { text-align: right; }
|
|
838
|
+
.bp-opt { font-family: var(--ks-mono); font-size: .62rem; letter-spacing: .12em; text-transform: uppercase; padding: 7px 12px; background: transparent; border: 0; color: var(--ks-text-faint); cursor: pointer; transition: color .2s ease, background-color .2s ease; }
|
|
839
|
+
.bp-opt + .bp-opt { border-left: 1px solid var(--ks-rule); }
|
|
840
|
+
.bp-opt.active { color: var(--ks-dark-ink); background: var(--ks-kinpaku-rich); }
|
|
841
|
+
.bp-opt:not(.active):hover { color: var(--ks-text); }
|
|
842
|
+
.bp-note { font-family: var(--ks-mono); font-size: .58rem; letter-spacing: .04em; color: var(--ks-text-faint); max-width: 21rem; line-height: 1.5; }
|
|
843
|
+
/* Flipping to comp starts billed, minutes-long generation, so it asks
|
|
844
|
+
first; flipping back is free and never does. */
|
|
845
|
+
#bp-confirm { position: fixed; inset: 0; z-index: 60; display: flex; align-items: center; justify-content: center; background: oklch(4% 0.004 95 / 0.72); opacity: 0; transition: opacity .2s ease; }
|
|
846
|
+
#bp-confirm[hidden] { display: none; }
|
|
847
|
+
#bp-confirm.open { opacity: 1; }
|
|
848
|
+
.bp-confirm-panel { max-width: 26rem; margin: 1rem; background: var(--ks-lacquer-raised); border: 1px solid var(--ks-rule); border-radius: 10px; padding: 1.4rem 1.5rem 1.3rem; box-shadow: 0 30px 80px oklch(0% 0 0 / 0.55); }
|
|
849
|
+
.bp-confirm-panel h2 { font-family: var(--ks-font); font-size: 1.125rem; font-weight: 500; color: var(--ks-champagne); margin-bottom: .55rem; }
|
|
850
|
+
.bp-confirm-panel p { font-size: .875rem; line-height: 1.55; color: var(--ks-text-muted); }
|
|
851
|
+
.bp-confirm-actions { display: flex; gap: .6rem; margin-top: 1.1rem; }
|
|
852
|
+
.bp-confirm-go { background: var(--ks-kinpaku); color: var(--ks-dark-ink); border: 0; font: inherit; font-weight: 500; padding: 9px 22px; border-radius: 6px; cursor: pointer; }
|
|
853
|
+
.bp-confirm-go:hover { background: var(--ks-kinpaku-pale); }
|
|
854
|
+
.bp-confirm-stay { background: transparent; color: var(--ks-text-muted); border: 1px solid var(--ks-rule); font: inherit; padding: 9px 18px; border-radius: 6px; cursor: pointer; }
|
|
855
|
+
.bp-confirm-stay:hover { color: var(--ks-text); border-color: var(--ks-text-faint); }
|
|
856
|
+
.reroll-btn { display: inline-flex; align-items: center; align-self: stretch; gap: 8px; padding: 0 16px; font-family: var(--ks-mono); font-size: .72rem; letter-spacing: .08em; text-transform: uppercase; color: var(--ks-kinpaku); background: transparent; border: 1px solid var(--ks-rule); border-radius: 6px; cursor: pointer; transition: border-color .2s ease, color .2s ease; }
|
|
857
|
+
.reroll-btn:hover { color: var(--ks-kinpaku-pale); border-color: var(--ks-kinpaku-deep); }
|
|
858
|
+
.reroll-btn svg { width: 15px; height: 15px; }
|
|
859
|
+
.reroll-btn[disabled] { opacity: .4; cursor: default; }
|
|
860
|
+
/* The register steers read quieter than the plain roll: they are exits from
|
|
861
|
+
the current register, not the round's main verbs. */
|
|
862
|
+
#reroll-safer, #reroll-bolder { color: var(--ks-text-muted); min-height: 38px; }
|
|
863
|
+
#reroll-safer:hover, #reroll-bolder:hover { color: var(--ks-text); border-color: var(--ks-text-faint); }
|
|
558
864
|
/* The quiet exit: always available, never argued with, visually subordinate
|
|
559
865
|
to the dealt cards and the re-roll so it reads as the user's own door,
|
|
560
866
|
not a recommendation. */
|
|
@@ -575,6 +881,17 @@ function page() {
|
|
|
575
881
|
<div id="ambient" aria-hidden="true"></div>
|
|
576
882
|
<div id="scrim" aria-hidden="true"></div>
|
|
577
883
|
<div id="lightbox" hidden><img alt=""></div>
|
|
884
|
+
<template id="tpl-expand-chip">${expandChip}</template>
|
|
885
|
+
${buildPath?.toggle ? `<div id="bp-confirm" role="dialog" aria-modal="true" aria-labelledby="bp-confirm-title" hidden>
|
|
886
|
+
<div class="bp-confirm-panel">
|
|
887
|
+
<h2 id="bp-confirm-title">Flip to comp-first?</h2>
|
|
888
|
+
<p>The agent starts rendering a comp for every open card right away, about a minute or two per card on your image provider, and the images land on the cards as they finish. This flip binds this session only.</p>
|
|
889
|
+
<div class="bp-confirm-actions">
|
|
890
|
+
<button type="button" class="bp-confirm-go" data-confirm>Render comps</button>
|
|
891
|
+
<button type="button" class="bp-confirm-stay" data-cancel>Keep code-first</button>
|
|
892
|
+
</div>
|
|
893
|
+
</div>
|
|
894
|
+
</div>` : ''}
|
|
578
895
|
<header>
|
|
579
896
|
<div class="brand">
|
|
580
897
|
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M5 2.5 L13.5 2.5 L5.5 21.5 L5 21.5 Q2.5 21.5 2.5 19 L2.5 5 Q2.5 2.5 5 2.5 Z"/><path d="M16.5 2.5 L19 2.5 Q21.5 2.5 21.5 5 L21.5 19 Q21.5 21.5 19 21.5 L8.5 21.5 Z"/></svg>
|
|
@@ -586,6 +903,13 @@ function page() {
|
|
|
586
903
|
<div class="headline">
|
|
587
904
|
<svg class="headline-die" viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="4" fill="none" stroke="currentColor" stroke-width="1.6"/><circle cx="8.4" cy="8.4" r="1.5" fill="currentColor"/><circle cx="15.6" cy="8.4" r="1.5" fill="currentColor"/><circle cx="8.4" cy="15.6" r="1.5" fill="currentColor"/><circle cx="15.6" cy="15.6" r="1.5" fill="currentColor"/><circle cx="12" cy="12" r="1.5" fill="currentColor"/></svg>
|
|
588
905
|
<h1>${esc(payload.title || 'Choose a direction')}</h1>
|
|
906
|
+
${buildPath?.toggle ? `<div id="build-path" data-default="${buildPath.value}">
|
|
907
|
+
<div class="bp-switch" role="radiogroup" aria-label="Build path">
|
|
908
|
+
<button type="button" class="bp-opt" data-bp="comp" role="radio" aria-checked="false">Comp first</button>
|
|
909
|
+
<button type="button" class="bp-opt" data-bp="code" role="radio" aria-checked="false">Code first</button>
|
|
910
|
+
</div>
|
|
911
|
+
<p class="bp-note" data-bp-note></p>
|
|
912
|
+
</div>` : ''}
|
|
589
913
|
</div>
|
|
590
914
|
${payload.question ? `<p class="question">${esc(payload.question)}</p>` : ''}
|
|
591
915
|
<div class="deck-shell">
|
|
@@ -599,16 +923,33 @@ function page() {
|
|
|
599
923
|
</main>
|
|
600
924
|
<footer>
|
|
601
925
|
${payload.steer ? '<input id="steer" placeholder="Optional steer: what should be different or kept?">' : ''}
|
|
602
|
-
${
|
|
926
|
+
${(() => {
|
|
927
|
+
if (!payload.reroll) return '';
|
|
928
|
+
const die = '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="4" fill="none" stroke="currentColor" stroke-width="1.6"/><circle cx="8.4" cy="8.4" r="1.5" fill="currentColor"/><circle cx="15.6" cy="8.4" r="1.5" fill="currentColor"/><circle cx="8.4" cy="15.6" r="1.5" fill="currentColor"/><circle cx="15.6" cy="15.6" r="1.5" fill="currentColor"/><circle cx="12" cy="12" r="1.5" fill="currentColor"/></svg>';
|
|
929
|
+
const registers = Array.isArray(payload.reroll.registers) ? payload.reroll.registers.filter((r) => r === 'safer' || r === 'bolder') : [];
|
|
930
|
+
// The registers are the user's steering wheel on the familiar-to-bold
|
|
931
|
+
// axis; the plain re-roll sits between them so the spatial order matches
|
|
932
|
+
// the axis it names.
|
|
933
|
+
const safer = registers.includes('safer') ? '<button class="reroll-btn" id="reroll-safer" title="Deal the familiar register: conventional grounded directions plus the category standard against named competitors"><span>← Safer hand</span></button>' : '';
|
|
934
|
+
const bolder = registers.includes('bolder') ? '<button class="reroll-btn" id="reroll-bolder" title="Deal foreign forms only, at full commitment"><span>Bolder hand →</span></button>' : '';
|
|
935
|
+
return `${safer}<button class="reroll-btn" id="reroll">${die}<span>Re-roll</span></button>${bolder}`;
|
|
936
|
+
})()}
|
|
603
937
|
${payload.canon && !payload.canonCard ? '<button id="canon" title="Skip the roll: build the page this category ships, executed impeccably">Play it straight</button>' : ''}
|
|
604
938
|
</footer>
|
|
605
939
|
<script>
|
|
606
940
|
const steer = () => document.getElementById('steer')?.value || '';
|
|
941
|
+
// A followup round's pick keeps the tab: the next round arrives via
|
|
942
|
+
// --update, so the page shows the loading hand instead of goodbye. Detached
|
|
943
|
+
// mode only, and the page must agree with the server: a blocking server
|
|
944
|
+
// exits on any pick and has no update channel, so a followup payload there
|
|
945
|
+
// still gets the goodbye screen, never a loading hand nothing will resolve.
|
|
946
|
+
const FOLLOWUP = ${payload.followup === true && Boolean(detachedKey) ? 'true' : 'false'};
|
|
607
947
|
const beat = () => { try { navigator.sendBeacon('/heartbeat'); } catch { fetch('/heartbeat', { method: 'POST' }); } };
|
|
608
948
|
beat();
|
|
609
949
|
setInterval(beat, 5000);
|
|
610
950
|
async function answer(optionId) {
|
|
611
951
|
await fetch('/answer', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ optionId, steer: steer() }) });
|
|
952
|
+
if (FOLLOWUP) { await awaitNextRound(); return; }
|
|
612
953
|
document.body.innerHTML = '<div class="done"><svg viewBox="0 0 24 24" width="38" height="38" fill="oklch(84% 0.19 80.46)" aria-hidden="true"><path d="M5 2.5 L13.5 2.5 L5.5 21.5 L5 21.5 Q2.5 21.5 2.5 19 L2.5 5 Q2.5 2.5 5 2.5 Z"/><path d="M16.5 2.5 L19 2.5 Q21.5 2.5 21.5 5 L21.5 19 Q21.5 21.5 19 21.5 L8.5 21.5 Z"/></svg>Choice recorded. The agent is resuming; you can close this tab.</div>';
|
|
613
954
|
}
|
|
614
955
|
document.querySelectorAll('button.choose').forEach(b => b.addEventListener('click', () => answer(b.dataset.id)));
|
|
@@ -617,6 +958,25 @@ function page() {
|
|
|
617
958
|
b.closest('.card').classList.toggle('flipped');
|
|
618
959
|
}));
|
|
619
960
|
|
|
961
|
+
// Raise cycler: click (or Enter) advances to the next donation.
|
|
962
|
+
document.querySelectorAll('.raises-cycle').forEach(cycle => {
|
|
963
|
+
const raises = [...cycle.querySelectorAll('.raise')];
|
|
964
|
+
const count = cycle.querySelector('[data-raises-count]');
|
|
965
|
+
let at = 0;
|
|
966
|
+
const live = cycle.querySelector('.sr-live');
|
|
967
|
+
const show = (announce) => {
|
|
968
|
+
raises.forEach((raise, i) => raise.classList.toggle('active', i === at));
|
|
969
|
+
if (count) count.textContent = (at + 1) + '/' + raises.length;
|
|
970
|
+
// Screen readers hear the raise they just advanced to; the initial
|
|
971
|
+
// render stays quiet so page load does not narrate every card.
|
|
972
|
+
if (announce && live) live.textContent = 'Improvement ' + (at + 1) + ' of ' + raises.length + ': ' + (raises[at]?.textContent || '');
|
|
973
|
+
};
|
|
974
|
+
show(false);
|
|
975
|
+
const advance = (e) => { e.stopPropagation(); at = (at + 1) % raises.length; show(true); };
|
|
976
|
+
cycle.addEventListener('click', advance);
|
|
977
|
+
cycle.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); advance(e); } });
|
|
978
|
+
});
|
|
979
|
+
|
|
620
980
|
// Deal from the stack: cards begin piled at the grid's center, blurred,
|
|
621
981
|
// then travel to their seats with a stagger.
|
|
622
982
|
const cards = [...document.querySelectorAll('.card')];
|
|
@@ -650,56 +1010,303 @@ function page() {
|
|
|
650
1010
|
}));
|
|
651
1011
|
}
|
|
652
1012
|
|
|
653
|
-
//
|
|
1013
|
+
// Comps stream in after the deal: poll each slot until the file lands,
|
|
654
1014
|
// then swap the shimmer for the image. Generation is genuinely slow and a
|
|
655
1015
|
// sequential batch puts the last card many minutes out, so patience is the
|
|
656
1016
|
// default: a slot only shows its inspiration as a stand-in when it has
|
|
657
1017
|
// waited four minutes AND nothing has landed anywhere for four minutes, the
|
|
658
|
-
// stand-in is labeled as such, and polling continues so the real
|
|
1018
|
+
// stand-in is labeled as such, and polling continues so the real comp
|
|
659
1019
|
// still swaps in whenever it arrives. Progress anywhere resets patience.
|
|
660
1020
|
const landTracker = { last: Date.now() };
|
|
661
|
-
|
|
662
|
-
const url = m.dataset.
|
|
663
|
-
|
|
664
|
-
|
|
1021
|
+
const pollComp = (m) => {
|
|
1022
|
+
const url = m.dataset.comp;
|
|
1023
|
+
// A converted slot is the SAME node across flip cycles, so a probe from an
|
|
1024
|
+
// earlier cycle can still be in flight when the next one starts. Without a
|
|
1025
|
+
// generation stamp its late onload settles the new slot: shimmer stripped,
|
|
1026
|
+
// pending state cleared, comp still hidden, live poll stopped.
|
|
1027
|
+
const generation = String(Number(m.dataset.pollGen || 0));
|
|
1028
|
+
const current = () => String(Number(m.dataset.pollGen || 0)) === generation;
|
|
1029
|
+
const img = m.querySelector('img.comp');
|
|
1030
|
+
const note = m.querySelector('.comp-note');
|
|
665
1031
|
const started = Date.now();
|
|
666
1032
|
// A live elapsed count is the difference between "working" and "frozen".
|
|
667
|
-
const tick = setInterval(() => { if (note) note.textContent = '
|
|
668
|
-
const settle = () => { clearInterval(tick); m.classList.remove('
|
|
669
|
-
const
|
|
1033
|
+
const tick = setInterval(() => { if (note) note.textContent = 'rendering · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000);
|
|
1034
|
+
const settle = () => { clearInterval(tick); m.classList.remove('comp-pending', 'stand-in'); m.querySelector('.shimmer')?.remove(); m.querySelector('.stand-in-label')?.remove(); };
|
|
1035
|
+
const fallback = () => {
|
|
670
1036
|
const pip = m.querySelector('.pip img');
|
|
671
|
-
if (
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
1037
|
+
if (pip) {
|
|
1038
|
+
if (m.classList.contains('stand-in')) return false;
|
|
1039
|
+
img.src = pip.getAttribute('src'); img.hidden = false;
|
|
1040
|
+
m.classList.add('stand-in');
|
|
1041
|
+
m.querySelector('.shimmer')?.remove();
|
|
1042
|
+
clearInterval(tick);
|
|
1043
|
+
const label = document.createElement('p');
|
|
1044
|
+
label.className = 'stand-in-label';
|
|
1045
|
+
label.textContent = 'inspiration · comp pending';
|
|
1046
|
+
m.appendChild(label);
|
|
1047
|
+
return false;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// No comp and no inspiration is the text-only card the payload would
|
|
1051
|
+
// have rendered without a comp declaration. Bring the complete read
|
|
1052
|
+
// forward before removing the now-unreachable back face.
|
|
1053
|
+
const card = m.closest('.card');
|
|
1054
|
+
const front = card?.querySelector('.face.front');
|
|
1055
|
+
const body = front?.querySelector('.body');
|
|
1056
|
+
const back = card?.querySelector('.face.back');
|
|
1057
|
+
const textOnlyFacts = m.querySelector('template.text-only-facts');
|
|
1058
|
+
const choose = body?.querySelector(':scope > button.choose');
|
|
1059
|
+
if (body && textOnlyFacts && choose) {
|
|
1060
|
+
const plainDetail = body.querySelector(':scope > .detail:not(.more)');
|
|
1061
|
+
[...body.children].filter((el) => el.classList.contains('fact') || el.matches('.detail.more')).forEach((el) => el.remove());
|
|
1062
|
+
choose.before(textOnlyFacts.content.cloneNode(true));
|
|
1063
|
+
if (plainDetail) choose.before(plainDetail);
|
|
1064
|
+
}
|
|
1065
|
+
card?.classList.remove('flipped');
|
|
1066
|
+
front?.classList.add('text-only');
|
|
1067
|
+
back?.remove();
|
|
1068
|
+
settle();
|
|
1069
|
+
m.remove();
|
|
1070
|
+
return true;
|
|
680
1071
|
};
|
|
681
1072
|
const tryLoad = () => {
|
|
1073
|
+
// A slot the user flipped back out of either leaves the DOM or, when it
|
|
1074
|
+
// was an inspiration face converted in place, stays and loses its
|
|
1075
|
+
// pending state. Either way its loop is done.
|
|
1076
|
+
if (!m.isConnected || !m.classList.contains('comp-pending') || !current()) { clearInterval(tick); return; }
|
|
682
1077
|
const probe = new Image();
|
|
683
|
-
probe.onload = () => {
|
|
1078
|
+
probe.onload = () => {
|
|
1079
|
+
// A stale generation also ends this run's clock. tryLoad clears it on
|
|
1080
|
+
// re-entry, and an in-flight probe that finishes stale schedules no
|
|
1081
|
+
// re-entry, so returning without clearing ran the interval for the rest
|
|
1082
|
+
// of the page's life.
|
|
1083
|
+
if (!current()) { clearInterval(tick); return; }
|
|
1084
|
+
landTracker.last = Date.now();
|
|
1085
|
+
const target = m.querySelector('img.comp') || img;
|
|
1086
|
+
target.src = probe.src;
|
|
1087
|
+
target.hidden = false;
|
|
1088
|
+
settle();
|
|
1089
|
+
};
|
|
684
1090
|
probe.onerror = () => {
|
|
1091
|
+
if (!current()) { clearInterval(tick); return; }
|
|
685
1092
|
const quiet = Date.now() - landTracker.last > 240000;
|
|
686
|
-
if (Date.now() - started > 240000 && quiet
|
|
1093
|
+
if (Date.now() - started > 240000 && quiet && fallback()) return;
|
|
687
1094
|
setTimeout(tryLoad, m.classList.contains('stand-in') ? 5000 : 2500);
|
|
688
1095
|
};
|
|
689
1096
|
probe.src = url + (url.includes('?') ? '&' : '?') + 't=' + Date.now();
|
|
690
1097
|
};
|
|
691
1098
|
tryLoad();
|
|
1099
|
+
};
|
|
1100
|
+
document.querySelectorAll('.media.comp-pending').forEach(pollComp);
|
|
1101
|
+
|
|
1102
|
+
// Build-path toggle: the default is the round's recorded preference and
|
|
1103
|
+
// flipping binds this session only. Flipping code to comp swaps every
|
|
1104
|
+
// reserve slot (data-comp-slot) to its shimmer and tells the server, so
|
|
1105
|
+
// the waiting agent starts generating; flipping back is free: pending
|
|
1106
|
+
// slots return to their wireframes, a comp that already landed stays.
|
|
1107
|
+
const bp = document.getElementById('build-path');
|
|
1108
|
+
if (bp) {
|
|
1109
|
+
const notes = {
|
|
1110
|
+
comp: 'An image sets the bar first and the build must match it. Bolder composition; comps render before code.',
|
|
1111
|
+
code: 'Code builds directly; the ambition is written into the contract and audited at the finish. Leaner, faster.',
|
|
1112
|
+
};
|
|
1113
|
+
const noteEl = bp.querySelector('[data-bp-note]');
|
|
1114
|
+
let current = bp.dataset.default;
|
|
1115
|
+
const set = (value) => {
|
|
1116
|
+
current = value;
|
|
1117
|
+
bp.querySelectorAll('.bp-opt').forEach(b => {
|
|
1118
|
+
const on = b.dataset.bp === value;
|
|
1119
|
+
b.classList.toggle('active', on);
|
|
1120
|
+
b.setAttribute('aria-checked', String(on));
|
|
1121
|
+
});
|
|
1122
|
+
if (noteEl) noteEl.textContent = notes[value];
|
|
1123
|
+
};
|
|
1124
|
+
set(current);
|
|
1125
|
+
const INSPO_TITLE = 'Inspiration: the world this direction draws from. Your page will not look like this image.';
|
|
1126
|
+
// Every media slot carries the expand affordance. A slot built here after
|
|
1127
|
+
// the deal used to ship without one, so a comp the user waited minutes for
|
|
1128
|
+
// could not be opened.
|
|
1129
|
+
const ensureChips = (m) => {
|
|
1130
|
+
if (m.querySelector('.chips')) return;
|
|
1131
|
+
const tpl = document.getElementById('tpl-expand-chip');
|
|
1132
|
+
if (!tpl) return;
|
|
1133
|
+
const chips = document.createElement('div');
|
|
1134
|
+
chips.className = 'chips';
|
|
1135
|
+
chips.appendChild(tpl.content.cloneNode(true));
|
|
1136
|
+
m.appendChild(chips);
|
|
1137
|
+
};
|
|
1138
|
+
const shimmerHtml = '<div class="shimmer"><span class="comp-note">rendering…</span></div><img class="comp" alt="" hidden>';
|
|
1139
|
+
const enterComp = () => {
|
|
1140
|
+
document.querySelectorAll('.card[data-comp-slot]').forEach(card => {
|
|
1141
|
+
const front = card.querySelector('.face.front');
|
|
1142
|
+
if (!front || front.querySelector('.media.comp-pending') || front.querySelector('.media img.comp:not([hidden])')) return;
|
|
1143
|
+
// On a code-led card the inspiration IS the face. Comp-first demotes it
|
|
1144
|
+
// to the corner, so convert that slot in place rather than inserting a
|
|
1145
|
+
// second one: two stacked images say the catalog art and the comp are
|
|
1146
|
+
// peers, and the whole point of the corner is that they are not.
|
|
1147
|
+
const inspo = front.querySelector('.media:not(.wire):not(.comp-pending)');
|
|
1148
|
+
let m;
|
|
1149
|
+
if (inspo) {
|
|
1150
|
+
m = inspo;
|
|
1151
|
+
m.dataset.compRestore = 'inspiration';
|
|
1152
|
+
m.dataset.comp = card.dataset.compSlot;
|
|
1153
|
+
m.classList.add('comp-pending');
|
|
1154
|
+
m.removeAttribute('title');
|
|
1155
|
+
m.querySelector('.media-label')?.remove();
|
|
1156
|
+
const art = m.querySelector(':scope > img');
|
|
1157
|
+
if (art) {
|
|
1158
|
+
const pip = document.createElement('figure');
|
|
1159
|
+
pip.className = 'pip';
|
|
1160
|
+
pip.title = INSPO_TITLE;
|
|
1161
|
+
const cap = document.createElement('figcaption');
|
|
1162
|
+
cap.textContent = 'inspiration';
|
|
1163
|
+
pip.append(art, cap);
|
|
1164
|
+
m.appendChild(pip);
|
|
1165
|
+
}
|
|
1166
|
+
m.insertAdjacentHTML('afterbegin', shimmerHtml);
|
|
1167
|
+
} else {
|
|
1168
|
+
m = document.createElement('div');
|
|
1169
|
+
m.className = 'media comp-pending';
|
|
1170
|
+
m.dataset.comp = card.dataset.compSlot;
|
|
1171
|
+
m.innerHTML = shimmerHtml;
|
|
1172
|
+
const wireEl = front.querySelector('.media.wire');
|
|
1173
|
+
if (wireEl) { wireEl.hidden = true; front.insertBefore(m, wireEl); }
|
|
1174
|
+
else { front.classList.remove('text-only'); front.insertBefore(m, front.querySelector('.body')); }
|
|
1175
|
+
}
|
|
1176
|
+
ensureChips(m);
|
|
1177
|
+
pollComp(m);
|
|
1178
|
+
});
|
|
1179
|
+
};
|
|
1180
|
+
const exitComp = () => {
|
|
1181
|
+
document.querySelectorAll('.card[data-comp-slot]').forEach(card => {
|
|
1182
|
+
const front = card.querySelector('.face.front');
|
|
1183
|
+
const pending = front?.querySelector('.media.comp-pending');
|
|
1184
|
+
if (!pending) return; // landed comps stay; they exist either way
|
|
1185
|
+
// A converted slot is restored, not removed: the inspiration goes back
|
|
1186
|
+
// to being the face, so flipping back leaves the card as it was dealt.
|
|
1187
|
+
if (pending.dataset.compRestore === 'inspiration') {
|
|
1188
|
+
// Everything the pending state added has to leave, presentation
|
|
1189
|
+
// included: a slot that reached stand-in kept its "inspiration comp
|
|
1190
|
+
// pending" label beside a fresh one, and a slot whose art had failed
|
|
1191
|
+
// came back still marked unavailable.
|
|
1192
|
+
pending.classList.remove('comp-pending', 'stand-in');
|
|
1193
|
+
delete pending.dataset.compRestore;
|
|
1194
|
+
delete pending.dataset.comp;
|
|
1195
|
+
pending.dataset.pollGen = String(Number(pending.dataset.pollGen || 0) + 1);
|
|
1196
|
+
pending.querySelector('.shimmer')?.remove();
|
|
1197
|
+
pending.querySelector('img.comp')?.remove();
|
|
1198
|
+
pending.querySelector('.stand-in-label')?.remove();
|
|
1199
|
+
pending.querySelectorAll('.media-label').forEach((el) => el.remove());
|
|
1200
|
+
const pip = pending.querySelector('.pip');
|
|
1201
|
+
const art = pip?.querySelector('img');
|
|
1202
|
+
if (art) pending.insertBefore(art, pending.firstChild);
|
|
1203
|
+
pip?.remove();
|
|
1204
|
+
// No art means the image failed to load before the flip, so hand the
|
|
1205
|
+
// slot back to the same honest treatment rather than calling it art.
|
|
1206
|
+
const label = document.createElement('p');
|
|
1207
|
+
label.className = 'media-label';
|
|
1208
|
+
label.textContent = art ? 'inspiration' : 'artwork unavailable';
|
|
1209
|
+
pending.insertBefore(label, pending.querySelector('.chips'));
|
|
1210
|
+
if (art) pending.title = INSPO_TITLE; else pending.classList.add('unavailable');
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
pending.remove();
|
|
1214
|
+
const wireEl = front.querySelector('.media.wire');
|
|
1215
|
+
if (wireEl) wireEl.hidden = false;
|
|
1216
|
+
else if (!front.querySelector('.media')) front.classList.add('text-only');
|
|
1217
|
+
});
|
|
1218
|
+
};
|
|
1219
|
+
const apply = (value) => {
|
|
1220
|
+
set(value);
|
|
1221
|
+
fetch('/build-path', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ value }) });
|
|
1222
|
+
if (value === 'comp') enterComp(); else exitComp();
|
|
1223
|
+
};
|
|
1224
|
+
// Flipping to comp starts real generation, so it confirms first; the
|
|
1225
|
+
// flip back is free and applies immediately.
|
|
1226
|
+
const confirm = document.getElementById('bp-confirm');
|
|
1227
|
+
const closeConfirm = () => { confirm.classList.remove('open'); confirm.hidden = true; };
|
|
1228
|
+
confirm.querySelector('[data-confirm]').addEventListener('click', () => { closeConfirm(); apply('comp'); });
|
|
1229
|
+
confirm.querySelector('[data-cancel]').addEventListener('click', closeConfirm);
|
|
1230
|
+
confirm.addEventListener('click', (e) => { if (e.target === confirm) closeConfirm(); });
|
|
1231
|
+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && !confirm.hidden) closeConfirm(); });
|
|
1232
|
+
bp.querySelectorAll('.bp-opt').forEach(b => b.addEventListener('click', () => {
|
|
1233
|
+
const value = b.dataset.bp;
|
|
1234
|
+
if (value === current) return;
|
|
1235
|
+
if (value === 'comp') {
|
|
1236
|
+
confirm.hidden = false;
|
|
1237
|
+
requestAnimationFrame(() => confirm.classList.add('open'));
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
apply(value);
|
|
1241
|
+
}));
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// A declared image that never loads (missing catalog asset, offline shell)
|
|
1245
|
+
// must not sit as a dark void: the slot collapses to the card's own
|
|
1246
|
+
// palette, labeled honestly, and the card competes on its facts. Sketch
|
|
1247
|
+
// slots are excluded; their polling owns the wait.
|
|
1248
|
+
const artFailed = (img) => {
|
|
1249
|
+
const m = img.closest('.media');
|
|
1250
|
+
if (!m || m.classList.contains('comp-pending') || m.classList.contains('unavailable')) return;
|
|
1251
|
+
m.classList.add('unavailable');
|
|
1252
|
+
const colors = [...(img.closest('.card')?.querySelectorAll('.swatches i') || [])].map(i => i.style.background).filter(Boolean);
|
|
1253
|
+
if (colors.length) m.style.background = 'linear-gradient(135deg, ' + colors.map((c, i) => c + ' ' + Math.round(i * 100 / colors.length) + '% ' + Math.round((i + 1) * 100 / colors.length) + '%').join(', ') + ')';
|
|
1254
|
+
m.querySelector('.media-label')?.remove();
|
|
1255
|
+
m.querySelector('.chip.expand')?.remove();
|
|
1256
|
+
m.removeAttribute('title');
|
|
1257
|
+
img.remove();
|
|
1258
|
+
const label = document.createElement('p');
|
|
1259
|
+
label.className = 'media-label';
|
|
1260
|
+
label.textContent = 'artwork unavailable';
|
|
1261
|
+
m.appendChild(label);
|
|
1262
|
+
};
|
|
1263
|
+
document.querySelectorAll('.media:not(.comp-pending) > img').forEach(img => {
|
|
1264
|
+
if (img.complete && img.naturalWidth === 0 && img.getAttribute('src')) artFailed(img);
|
|
1265
|
+
else img.addEventListener('error', () => artFailed(img), { once: true });
|
|
1266
|
+
});
|
|
1267
|
+
// A broken inspiration PIP or thumb just leaves; nothing depends on it.
|
|
1268
|
+
document.querySelectorAll('.pip img, .inspo img').forEach(img => {
|
|
1269
|
+
const gone = () => img.closest('.pip, .inspo')?.remove();
|
|
1270
|
+
if (img.complete && img.naturalWidth === 0) gone();
|
|
1271
|
+
else img.addEventListener('error', gone, { once: true });
|
|
692
1272
|
});
|
|
693
1273
|
|
|
694
|
-
//
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
1274
|
+
// Every zoom target resolves in ONE delegated listener, in priority order.
|
|
1275
|
+
// Delegation is what lets a slot built by a build-path flip work at all, and
|
|
1276
|
+
// a single listener is what keeps the targets from fighting: stopPropagation
|
|
1277
|
+
// ends bubbling, not other listeners on the same target, so split across two
|
|
1278
|
+
// handlers a click on the corner inspiration opened the inspiration and then
|
|
1279
|
+
// the comp overwrote it in the lightbox. Per-element handlers elsewhere (the
|
|
1280
|
+
// flip chip, the raise cycler) still stop bubbling before the event lands
|
|
1281
|
+
// here, so they keep their own behavior.
|
|
1282
|
+
document.addEventListener('click', (e) => {
|
|
1283
|
+
const target = e.target;
|
|
1284
|
+
if (!target || !target.closest) return;
|
|
1285
|
+
// The corner inspiration wins over the slot it sits inside.
|
|
1286
|
+
const pip = target.closest('.pip, .inspo');
|
|
1287
|
+
if (pip) {
|
|
1288
|
+
const art = pip.querySelector('img');
|
|
1289
|
+
if (art) openLightbox(art);
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
const chip = target.closest('.chip');
|
|
1293
|
+
if (chip) {
|
|
1294
|
+
// Only expand zooms. Any other chip owns its click and must not fall
|
|
1295
|
+
// through to the media underneath it.
|
|
1296
|
+
if (!chip.classList.contains('expand')) return;
|
|
1297
|
+
const card = chip.closest('.card');
|
|
1298
|
+
const face = card && card.classList.contains('flipped') ? '.face.back' : '.face.front';
|
|
1299
|
+
const shown = card && card.querySelector(face + ' .media img:not([hidden])');
|
|
1300
|
+
if (shown && shown.getAttribute('src')) openLightbox(shown);
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
// The whole image is the zoom target, not just the chip; the chip stays as
|
|
1304
|
+
// the visible affordance.
|
|
1305
|
+
const media = target.closest('.media');
|
|
1306
|
+
if (!media) return;
|
|
1307
|
+
const art = media.querySelector(':scope > img:not([hidden])');
|
|
1308
|
+
if (art && art.getAttribute('src')) openLightbox(art);
|
|
1309
|
+
});
|
|
703
1310
|
|
|
704
1311
|
// Deck paging: arrows appear only when the deck overflows its axis, page
|
|
705
1312
|
// one card at a time, and follow the aspect-ratio flip between row and column.
|
|
@@ -739,7 +1346,7 @@ function page() {
|
|
|
739
1346
|
const ambient = document.getElementById('ambient');
|
|
740
1347
|
document.querySelectorAll('.card').forEach(card => {
|
|
741
1348
|
card.addEventListener('mouseenter', () => {
|
|
742
|
-
const art = card.querySelector('.face.front .media img:not([hidden])') || card.querySelector('.face.front .pip img');
|
|
1349
|
+
const art = card.querySelector('.face.front .media img:not([hidden])') || card.querySelector('.face.front .pip img') || card.querySelector('.face.front .inspo img');
|
|
743
1350
|
if (!art || !art.getAttribute('src')) return;
|
|
744
1351
|
ambient.style.backgroundImage = 'url("' + art.getAttribute('src') + '")'; ambient.style.opacity = '1';
|
|
745
1352
|
});
|
|
@@ -749,22 +1356,35 @@ function page() {
|
|
|
749
1356
|
// Expand: lightbox for whichever face is showing.
|
|
750
1357
|
const lightbox = document.getElementById('lightbox');
|
|
751
1358
|
const lightboxImg = lightbox.querySelector('img');
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
const face = card.classList.contains('flipped') ? '.face.back' : '.face.front';
|
|
756
|
-
const img = card.querySelector(face + ' .media img:not([hidden])');
|
|
757
|
-
if (!img || !img.getAttribute('src')) return;
|
|
1359
|
+
// Declared, not assigned to a const, so the delegated handlers above can call
|
|
1360
|
+
// it wherever they sit in this file.
|
|
1361
|
+
function openLightbox(img) {
|
|
758
1362
|
lightboxImg.src = img.getAttribute('src');
|
|
759
1363
|
lightbox.hidden = false;
|
|
760
1364
|
requestAnimationFrame(() => lightbox.classList.add('open'));
|
|
761
|
-
}
|
|
1365
|
+
}
|
|
1366
|
+
// Portrait art (native / mobile-first surfaces): the slot takes the
|
|
1367
|
+
// image's own ratio so nothing crops, and the whole deck narrows so
|
|
1368
|
+
// portrait cards sit side by side. Load events don't bubble; capture.
|
|
1369
|
+
document.addEventListener('load', (e) => {
|
|
1370
|
+
const img = e.target;
|
|
1371
|
+
if (!(img instanceof HTMLImageElement) || !img.matches('.media > img')) return;
|
|
1372
|
+
if (img.naturalHeight > img.naturalWidth * 1.05) {
|
|
1373
|
+
const m = img.closest('.media');
|
|
1374
|
+
m.classList.add('portrait');
|
|
1375
|
+
m.style.aspectRatio = img.naturalWidth + ' / ' + img.naturalHeight;
|
|
1376
|
+
document.querySelector('.grid')?.classList.add('portrait-media');
|
|
1377
|
+
}
|
|
1378
|
+
}, true);
|
|
762
1379
|
const closeLightbox = () => { lightbox.classList.remove('open'); setTimeout(() => { lightbox.hidden = true; }, 250); };
|
|
763
1380
|
lightbox.addEventListener('click', closeLightbox);
|
|
764
1381
|
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && !lightbox.hidden) closeLightbox(); });
|
|
765
1382
|
document.getElementById('canon')?.addEventListener('click', () => answer('canon'));
|
|
766
|
-
|
|
767
|
-
await fetch('/answer', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ optionId: 'reroll', steer: steer() }) });
|
|
1383
|
+
const dealAgain = async (register) => {
|
|
1384
|
+
await fetch('/answer', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ optionId: 'reroll', steer: steer(), ...(register ? { register } : {}) }) });
|
|
1385
|
+
await awaitNextRound();
|
|
1386
|
+
};
|
|
1387
|
+
async function awaitNextRound() {
|
|
768
1388
|
const grid = document.querySelector('.grid');
|
|
769
1389
|
const cardsNow = [...grid.querySelectorAll('.card')];
|
|
770
1390
|
const g = grid.getBoundingClientRect();
|
|
@@ -781,14 +1401,17 @@ function page() {
|
|
|
781
1401
|
}
|
|
782
1402
|
const cardHeight = cardsNow[0] ? cardsNow[0].getBoundingClientRect().height : 0;
|
|
783
1403
|
grid.innerHTML = cardsNow.map(() => '<article class="card skeleton"' + (cardHeight ? ' style="height:' + cardHeight + 'px"' : '') + '><div class="card-inner"><div class="face front"><div class="media"><div class="shimmer"></div></div><div class="body"><div class="line tier w40"></div><div class="line title w70"></div><div class="line w90"></div><div class="line w80"></div><div class="line w60"></div><div class="line button"></div></div></div></div></article>').join('');
|
|
784
|
-
document.
|
|
1404
|
+
document.querySelectorAll('.reroll-btn').forEach(b => b.setAttribute('disabled', ''));
|
|
785
1405
|
const poll = setInterval(async () => {
|
|
786
1406
|
try {
|
|
787
1407
|
const status = await (await fetch('/next-status')).json();
|
|
788
1408
|
if (status.ready) { clearInterval(poll); location.reload(); }
|
|
789
1409
|
} catch { /* server briefly busy */ }
|
|
790
1410
|
}, 1200);
|
|
791
|
-
}
|
|
1411
|
+
}
|
|
1412
|
+
document.getElementById('reroll')?.addEventListener('click', () => dealAgain());
|
|
1413
|
+
document.getElementById('reroll-safer')?.addEventListener('click', () => dealAgain('safer'));
|
|
1414
|
+
document.getElementById('reroll-bolder')?.addEventListener('click', () => dealAgain('bolder'));
|
|
792
1415
|
</script>`;
|
|
793
1416
|
}
|
|
794
1417
|
|
|
@@ -836,6 +1459,26 @@ const server = http.createServer((req, res) => {
|
|
|
836
1459
|
fs.createReadStream(abs).pipe(res);
|
|
837
1460
|
return;
|
|
838
1461
|
}
|
|
1462
|
+
if (req.method === 'POST' && req.url === '/build-path') {
|
|
1463
|
+
let body = '';
|
|
1464
|
+
req.on('data', (chunk) => { body += chunk; });
|
|
1465
|
+
req.on('end', () => {
|
|
1466
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
1467
|
+
res.end('{"ok":true}');
|
|
1468
|
+
let value = null;
|
|
1469
|
+
try { value = JSON.parse(body).value; } catch { /* ignore */ }
|
|
1470
|
+
if (value !== 'comp' && value !== 'code') return;
|
|
1471
|
+
const wasComp = liveBuildPath === 'comp';
|
|
1472
|
+
liveBuildPath = value;
|
|
1473
|
+
// Only a flip TO comp needs the agent mid-round: comps must start
|
|
1474
|
+
// rendering into the declared slots. The reverse is free.
|
|
1475
|
+
if (detachedKey && value === 'comp' && !wasComp) {
|
|
1476
|
+
fs.mkdirSync(QUESTION_DIR, { recursive: true });
|
|
1477
|
+
fs.writeFileSync(flipFile(detachedKey), JSON.stringify({ buildPath: 'comp' }) + '\n');
|
|
1478
|
+
}
|
|
1479
|
+
});
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
839
1482
|
if (req.method === 'POST' && req.url === '/answer') {
|
|
840
1483
|
let body = '';
|
|
841
1484
|
req.on('data', (chunk) => { body += chunk; });
|
|
@@ -845,22 +1488,30 @@ const server = http.createServer((req, res) => {
|
|
|
845
1488
|
let parsed = {};
|
|
846
1489
|
try { parsed = JSON.parse(body); } catch { /* empty steer */ }
|
|
847
1490
|
const chosen = options.find((o) => o.id === parsed.optionId);
|
|
1491
|
+
const isReroll = parsed.optionId === 'reroll';
|
|
1492
|
+
// A followup round's pick is not terminal: the table stays open for the
|
|
1493
|
+
// next round (--update), exactly like a re-roll. Detached mode only;
|
|
1494
|
+
// the blocking mode has no update channel, so its picks stay terminal.
|
|
1495
|
+
const followupOpen = Boolean(detachedKey) && payload.followup === true && !isReroll;
|
|
848
1496
|
const answer = JSON.stringify({
|
|
849
1497
|
optionId: parsed.optionId ?? null,
|
|
850
1498
|
steer: parsed.steer ?? '',
|
|
1499
|
+
...(isReroll && (parsed.register === 'safer' || parsed.register === 'bolder') ? { register: parsed.register } : {}),
|
|
1500
|
+
...(followupOpen ? { followup: true } : {}),
|
|
851
1501
|
...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}),
|
|
852
|
-
...(chosen?.sketch ? {
|
|
1502
|
+
...((chosen?.comp ?? chosen?.sketch) ? { comp: chosen.comp ?? chosen.sketch } : {}),
|
|
1503
|
+
...(liveBuildPath && !isReroll ? { buildPath: liveBuildPath, buildPathFlipped: liveBuildPath !== (buildPathDefault?.value ?? null) } : {}),
|
|
853
1504
|
});
|
|
854
|
-
const isReroll = parsed.optionId === 'reroll';
|
|
855
1505
|
if (detachedKey) {
|
|
856
1506
|
fs.mkdirSync(QUESTION_DIR, { recursive: true });
|
|
857
1507
|
fs.writeFileSync(answerFile(detachedKey), answer + '\n');
|
|
858
1508
|
} else {
|
|
859
1509
|
printAnswer(answer);
|
|
860
1510
|
}
|
|
861
|
-
// A re-roll in detached mode keeps the table open: the
|
|
862
|
-
// loading hand and reloads when --update delivers the
|
|
863
|
-
|
|
1511
|
+
// A re-roll or followup pick in detached mode keeps the table open: the
|
|
1512
|
+
// client shows a loading hand and reloads when --update delivers the
|
|
1513
|
+
// next round.
|
|
1514
|
+
if (!((isReroll || followupOpen) && detachedKey)) setTimeout(() => process.exit(0), 150);
|
|
864
1515
|
});
|
|
865
1516
|
return;
|
|
866
1517
|
}
|
|
@@ -878,8 +1529,7 @@ server.listen(portArg, '127.0.0.1', () => {
|
|
|
878
1529
|
console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...');
|
|
879
1530
|
}
|
|
880
1531
|
if (!hasFlag('no-open')) {
|
|
881
|
-
|
|
882
|
-
try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ }
|
|
1532
|
+
openSystemBrowser(url);
|
|
883
1533
|
}
|
|
884
1534
|
if (timeoutSec > 0) {
|
|
885
1535
|
setTimeout(() => {
|