savepoint 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +20 -0
- package/.prettierignore +4 -0
- package/.savepoint/Design.md +196 -0
- package/.savepoint/PRD.md +58 -0
- package/.savepoint/audit/E01-go-setup/proposals.md +166 -0
- package/.savepoint/audit/E01-go-setup/snapshot.md +71 -0
- package/.savepoint/audit/E01-scaffolding/proposals/AGENTS.md +66 -0
- package/.savepoint/audit/E01-scaffolding/proposals/Design.md +210 -0
- package/.savepoint/audit/E01-scaffolding/proposals/epic-Design.md +117 -0
- package/.savepoint/audit/E01-scaffolding/proposals/quality-review.md +101 -0
- package/.savepoint/audit/E01-scaffolding/snapshot.md +54 -0
- package/.savepoint/audit/E02-data-model/snapshot.md +128 -0
- package/.savepoint/audit/E02-data-readers/proposals.md +123 -0
- package/.savepoint/audit/E02-data-readers/snapshot.md +54 -0
- package/.savepoint/audit/E03-board-tui-core/proposals.md +146 -0
- package/.savepoint/audit/E03-board-tui-core/snapshot.md +57 -0
- package/.savepoint/audit/E03-cli-foundation/snapshot.md +106 -0
- package/.savepoint/audit/E04-board-components/proposals.md +118 -0
- package/.savepoint/audit/E04-board-components/snapshot.md +77 -0
- package/.savepoint/audit/E04-templates-and-prompts/snapshot.md +115 -0
- package/.savepoint/audit/E05-init-command/snapshot.md +125 -0
- package/.savepoint/audit/E05-phase-transitions/proposals.md +83 -0
- package/.savepoint/audit/E05-phase-transitions/snapshot.md +36 -0
- package/.savepoint/audit/E06-tui-board/snapshot.md +64 -0
- package/.savepoint/audit/E07-audit-pipeline/snapshot.md +165 -0
- package/.savepoint/audit/E08-board-workflow-cleanup/snapshot.md +65 -0
- package/.savepoint/config.yml +27 -0
- package/.savepoint/releases/v1/PRD.md +66 -0
- package/.savepoint/releases/v1/epics/E01-go-setup/Design.md +39 -0
- package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T001-init-module.md +42 -0
- package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T002-entrypoint.md +23 -0
- package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T003-directory-structure.md +24 -0
- package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T004-makefile.md +23 -0
- package/.savepoint/releases/v1/epics/E02-data-readers/Design.md +61 -0
- package/.savepoint/releases/v1/epics/E02-data-readers/tasks/T001-task-struct.md +29 -0
- package/.savepoint/releases/v1/epics/E02-data-readers/tasks/T002-frontmatter-parser.md +30 -0
- package/.savepoint/releases/v1/epics/E02-data-readers/tasks/T003-router-reader.md +29 -0
- package/.savepoint/releases/v1/epics/E02-data-readers/tasks/T004-config-reader.md +29 -0
- package/.savepoint/releases/v1/epics/E02-data-readers/tasks/T005-discovery.md +30 -0
- package/.savepoint/releases/v1/epics/E03-board-tui-core/Design.md +38 -0
- package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T001-model.md +29 -0
- package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T002-update-loop.md +30 -0
- package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T003-view.md +34 -0
- package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T004-styles.md +29 -0
- package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T005-layout.md +42 -0
- package/.savepoint/releases/v1/epics/E04-board-components/Design.md +44 -0
- package/.savepoint/releases/v1/epics/E04-board-components/tasks/T001-column.md +34 -0
- package/.savepoint/releases/v1/epics/E04-board-components/tasks/T002-card.md +33 -0
- package/.savepoint/releases/v1/epics/E04-board-components/tasks/T003-epic-panel.md +49 -0
- package/.savepoint/releases/v1/epics/E04-board-components/tasks/T004-detail-overlay.md +40 -0
- package/.savepoint/releases/v1/epics/E04-board-components/tasks/T005-release-dropdown.md +33 -0
- package/.savepoint/releases/v1/epics/E04-board-components/tasks/T006-help-overlay.md +34 -0
- package/.savepoint/releases/v1/epics/E05-phase-transitions/Design.md +38 -0
- package/.savepoint/releases/v1/epics/E05-phase-transitions/tasks/T001-phase-stepping.md +29 -0
- package/.savepoint/releases/v1/epics/E05-phase-transitions/tasks/T002-gates.md +31 -0
- package/.savepoint/releases/v1/epics/E05-phase-transitions/tasks/T003-write-task.md +31 -0
- package/.savepoint/releases/v1/epics/E05-phase-transitions/tasks/T004-write-router.md +31 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/Design.md +42 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T001-color-system.md +39 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T002-header-and-dividers.md +52 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T003-footer-status-bar.md +52 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T004-component-refinement.md +53 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T005-restore-nav-hints.md +39 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T007-detail-card-fixes.md +36 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T008-checkbox-states.md +38 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T009-router-priority-marker.md +41 -0
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T010-auto-refresh-watcher.md +61 -0
- package/.savepoint/releases/v1/epics/_archived/E01-archive-and-reset/Design.md +39 -0
- package/.savepoint/releases/v1/epics/_archived/E01-archive-and-reset/tasks/T001-archive-epics.md +20 -0
- package/.savepoint/releases/v1/epics/_archived/E01-archive-and-reset/tasks/T002-rewrite-prd.md +22 -0
- package/.savepoint/releases/v1/epics/_archived/E01-archive-and-reset/tasks/T003-create-epic-stubs.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E01-archive-and-reset/tasks/T004-update-router.md +22 -0
- package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/Design.md +118 -0
- package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/handoff.md +9 -0
- package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/tasks/T001-package-baseline.md +45 -0
- package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/tasks/T002-typescript-build.md +48 -0
- package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/tasks/T003-vitest-smoke.md +43 -0
- package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/tasks/T004-lint-format-gates.md +45 -0
- package/.savepoint/releases/v1/epics/_archived/E01-scaffolding/tasks/T005-scaffold-verification.md +40 -0
- package/.savepoint/releases/v1/epics/_archived/E02-data-model/Design.md +142 -0
- package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T001-domain-ids-status.md +27 -0
- package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T002-markdown-frontmatter-boundary.md +28 -0
- package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T003-task-documents.md +29 -0
- package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T004-release-epic-router-config-readers.md +30 -0
- package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T005-dependency-validation.md +29 -0
- package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T006-epic-task-set-reader.md +29 -0
- package/.savepoint/releases/v1/epics/_archived/E02-data-model/tasks/T007-quality-gates.md +31 -0
- package/.savepoint/releases/v1/epics/_archived/E02-domain-phase-model/Design.md +40 -0
- package/.savepoint/releases/v1/epics/_archived/E02-domain-phase-model/tasks/T001-phase-types.md +27 -0
- package/.savepoint/releases/v1/epics/_archived/E02-domain-phase-model/tasks/T002-phase-frontmatter.md +25 -0
- package/.savepoint/releases/v1/epics/_archived/E02-domain-phase-model/tasks/T003-simplify-config.md +26 -0
- package/.savepoint/releases/v1/epics/_archived/E02-domain-phase-model/tasks/T004-simplify-router-domain.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/Design.md +122 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T001-argument-parser-contract.md +28 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T002-help-text-generation.md +28 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T003-terminal-environment-detection.md +27 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T004-command-stub-modules.md +29 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T005-cli-runner-dispatch.md +34 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-foundation/tasks/T006-entrypoint-quality-gates.md +32 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/Design.md +43 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/tasks/T001-strip-args.md +26 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/tasks/T002-strip-help.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/tasks/T003-strip-run.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/tasks/T004-delete-commands.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E03-cli-simplify/tasks/T005-update-cli-tests.md +22 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/Design.md +48 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T001-board-data-phases.md +26 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T002-phase-rendering.md +28 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T003-detail-pane-phases.md +27 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T004-phase-transitions.md +42 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T005-phase-gates.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T006-phase-write-back.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T007-remove-audit-flow.md +27 -0
- package/.savepoint/releases/v1/epics/_archived/E04-board-phase-integration/tasks/T008-board-tests.md +25 -0
- package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/Design.md +85 -0
- package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/tasks/T001-project-template-assets.md +17 -0
- package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/tasks/T002-release-and-prompt-assets.md +20 -0
- package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/tasks/T003-template-registry-renderer.md +22 -0
- package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/tasks/T004-template-integrity-tests.md +17 -0
- package/.savepoint/releases/v1/epics/_archived/E04-templates-and-prompts/tasks/T005-template-closeout-quality-gates.md +16 -0
- package/.savepoint/releases/v1/epics/_archived/E05-init-command/Design.md +88 -0
- package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T001-init-cli-contract.md +22 -0
- package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T002-target-validation.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T003-scaffold-writer.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T004-magic-prompt-and-clipboard.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T005-dev-deps-install-option.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E05-init-command/tasks/T006-init-command-integration.md +28 -0
- package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/Design.md +53 -0
- package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T001-delete-dead-src.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T002-delete-dead-tests.md +26 -0
- package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T003-delete-assets.md +25 -0
- package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T004-clean-savepoint.md +28 -0
- package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T005-rewrite-agents-md.md +28 -0
- package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T006-clean-package-json.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E05-project-cleanup/tasks/T007-verify.md +25 -0
- package/.savepoint/releases/v1/epics/_archived/E06-tui-board/Design.md +104 -0
- package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T001-board-command-data.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T002-board-view-state.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T003-transition-gates-and-writes.md +25 -0
- package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T004-terminal-theme.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T005-ink-board-ui.md +26 -0
- package/.savepoint/releases/v1/epics/_archived/E06-tui-board/tasks/T006-board-integration-audit-entry.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/Design.md +88 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T001-audit-cli-contract.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T002-quality-gate-runner.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T003-snapshot-and-prompt.md +23 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T004-audit-orchestration-router.md +27 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T005-proposal-validation-apply.md +25 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T006-audit-review-state.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T007-audit-review-ui.md +26 -0
- package/.savepoint/releases/v1/epics/_archived/E07-audit-pipeline/tasks/T008-audit-pipeline-integration.md +24 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/Design.md +103 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T001-acceptance-criteria-model.md +30 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T002-release-task-set-reader.md +33 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T003-board-data-and-plain-output.md +34 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T004-board-selection-state.md +33 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T005-ink-board-layout-cleanup.md +37 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T006-task-detail-popup.md +36 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T007-templates-acceptance-criteria.md +34 -0
- package/.savepoint/releases/v1/epics/_archived/E08-board-workflow-cleanup/tasks/T008-board-workflow-integration.md +41 -0
- package/.savepoint/releases/v1/epics/_archived/E09-doctor-command/Design.md +70 -0
- package/.savepoint/releases/v1/epics/_archived/E10-docs-and-packaging/Design.md +68 -0
- package/.savepoint/releases/v1/epics/_archived/E11-release-validation/Design.md +68 -0
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/Design.md +26 -0
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T001-border-resize-fix.md +35 -0
- package/.savepoint/router.md +136 -0
- package/.savepoint/visual-identity.md +124 -0
- package/AGENTS.md +141 -0
- package/CLAUDE.md +1 -0
- package/GEMINI.md +1 -0
- package/LICENSE +21 -0
- package/Makefile +13 -0
- package/README.md +78 -0
- package/agent-skills/ink-tui-design/SKILL.md +309 -0
- package/agent-skills/ink-tui-design/references/component-patterns.md +371 -0
- package/agent-skills/ink-tui-design/references/hooks-guide.md +436 -0
- package/agent-skills/ink-tui-design/references/ink-gotchas.md +330 -0
- package/agent-skills/ink-tui-design/references/testing-patterns.md +384 -0
- package/agent-skills/savepoint-audit/SKILL.md +35 -0
- package/agent-skills/savepoint-build-task/SKILL.md +39 -0
- package/agent-skills/savepoint-create-plan/SKILL.md +28 -0
- package/agent-skills/savepoint-create-task/SKILL.md +31 -0
- package/agent-skills/savepoint-draft-prd/SKILL.md +32 -0
- package/agent-skills/savepoint-system-design/SKILL.md +33 -0
- package/agent-skills/superpowers/brainstorming/SKILL.md +165 -0
- package/agent-skills/superpowers/brainstorming/visual-companion.md +304 -0
- package/agent-skills/superpowers/dispatching-parallel-agents/SKILL.md +193 -0
- package/agent-skills/superpowers/executing-plans/SKILL.md +77 -0
- package/agent-skills/superpowers/finishing-a-development-branch/SKILL.md +213 -0
- package/agent-skills/superpowers/receiving-code-review/SKILL.md +226 -0
- package/agent-skills/superpowers/requesting-code-review/SKILL.md +115 -0
- package/agent-skills/superpowers/requesting-code-review/code-reviewer.md +160 -0
- package/agent-skills/superpowers/subagent-driven-development/SKILL.md +292 -0
- package/agent-skills/superpowers/subagent-driven-development/code-quality-reviewer-prompt.md +27 -0
- package/agent-skills/superpowers/subagent-driven-development/implementer-prompt.md +113 -0
- package/agent-skills/superpowers/subagent-driven-development/spec-reviewer-prompt.md +61 -0
- package/agent-skills/superpowers/systematic-debugging/SKILL.md +305 -0
- package/agent-skills/superpowers/systematic-debugging/condition-based-waiting.md +122 -0
- package/agent-skills/superpowers/systematic-debugging/defense-in-depth.md +130 -0
- package/agent-skills/superpowers/systematic-debugging/root-cause-tracing.md +183 -0
- package/agent-skills/superpowers/test-driven-development/SKILL.md +389 -0
- package/agent-skills/superpowers/test-driven-development/testing-anti-patterns.md +317 -0
- package/agent-skills/superpowers/verification-before-completion/SKILL.md +147 -0
- package/agent-skills/superpowers/writing-plans/SKILL.md +159 -0
- package/agent-skills/superpowers/writing-plans/plan-document-reviewer-prompt.md +49 -0
- package/assets/banner.png +0 -0
- package/assets/logo.png +0 -0
- package/assets/strawman.png +0 -0
- package/go.mod +33 -0
- package/go.sum +73 -0
- package/ink-cli-ui-design.zip +0 -0
- package/internal/board/board.go +121 -0
- package/internal/board/board_test.go +99 -0
- package/internal/board/card.go +72 -0
- package/internal/board/card_test.go +111 -0
- package/internal/board/column.go +61 -0
- package/internal/board/column_test.go +81 -0
- package/internal/board/detail.go +140 -0
- package/internal/board/detail_test.go +233 -0
- package/internal/board/epic_panel.go +69 -0
- package/internal/board/epic_panel_test.go +246 -0
- package/internal/board/help.go +40 -0
- package/internal/board/help_test.go +85 -0
- package/internal/board/layout.go +58 -0
- package/internal/board/layout_test.go +89 -0
- package/internal/board/model.go +151 -0
- package/internal/board/model_test.go +67 -0
- package/internal/board/release.go +42 -0
- package/internal/board/release_test.go +177 -0
- package/internal/board/transitions.go +88 -0
- package/internal/board/transitions_test.go +141 -0
- package/internal/board/update.go +155 -0
- package/internal/board/update_test.go +128 -0
- package/internal/board/view.go +190 -0
- package/internal/board/view_test.go +147 -0
- package/internal/data/config.go +87 -0
- package/internal/data/config_test.go +73 -0
- package/internal/data/discover.go +152 -0
- package/internal/data/discover_test.go +106 -0
- package/internal/data/errors.go +9 -0
- package/internal/data/lifecycle.go +37 -0
- package/internal/data/lifecycle_test.go +38 -0
- package/internal/data/parser.go +189 -0
- package/internal/data/parser_test.go +216 -0
- package/internal/data/router.go +52 -0
- package/internal/data/router_test.go +35 -0
- package/internal/data/task.go +46 -0
- package/internal/data/task_test.go +51 -0
- package/internal/data/write.go +144 -0
- package/internal/data/write_test.go +456 -0
- package/internal/styles/palette.go +47 -0
- package/internal/styles/styles.go +122 -0
- package/main.exe +0 -0
- package/main.go +11 -0
- package/package.json +25 -0
- package/savepoint +0 -0
- package/savepoint.exe +0 -0
- package/scripts/vitest-preload.cjs +95 -0
- package/templates/project/.savepoint/Design.md +47 -0
- package/templates/project/.savepoint/PRD.md +34 -0
- package/templates/project/.savepoint/config.yml +27 -0
- package/templates/project/.savepoint/router.md +152 -0
- package/templates/project/.savepoint/visual-identity.md +122 -0
- package/templates/project/AGENTS.md +130 -0
- package/templates/prompts/audit-reconciliation.prompt.md +67 -0
- package/templates/prompts/design.prompt.md +43 -0
- package/templates/prompts/epic-design.prompt.md +43 -0
- package/templates/prompts/magic-prompt.prompt.md +7 -0
- package/templates/prompts/prd.prompt.md +42 -0
- package/templates/prompts/task-breakdown.prompt.md +54 -0
- package/templates/prompts/task-building.prompt.md +38 -0
- package/templates/prompts/task-planning.prompt.md +53 -0
- package/templates/release/v1/PRD.md +37 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
|
|
6
|
+
"github.com/opencode/savepoint/internal/styles"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
const helpBorderPad = 4 // rounded border (2) + padding (2x1)
|
|
10
|
+
|
|
11
|
+
// RenderHelp renders the keyboard shortcut reference overlay.
|
|
12
|
+
func RenderHelp(width int) string {
|
|
13
|
+
inner := width - helpBorderPad
|
|
14
|
+
if inner < 4 {
|
|
15
|
+
inner = 4
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
lines := []string{
|
|
19
|
+
styles.ColumnTitleFocused.Render("KEYBOARD SHORTCUTS"),
|
|
20
|
+
strings.Repeat("─", inner),
|
|
21
|
+
helpRow("h / left", "previous column"),
|
|
22
|
+
helpRow("l / right", "next column"),
|
|
23
|
+
helpRow("enter", "open task detail / select item"),
|
|
24
|
+
helpRow("e", "open epic selector on narrow screens"),
|
|
25
|
+
helpRow("r", "open release selector"),
|
|
26
|
+
helpRow("up / k", "move selector up"),
|
|
27
|
+
helpRow("down / j", "move selector down"),
|
|
28
|
+
helpRow("?", "open help"),
|
|
29
|
+
helpRow("esc / q", "close overlay"),
|
|
30
|
+
helpRow("q / ctrl+c", "quit from board"),
|
|
31
|
+
"",
|
|
32
|
+
styles.CardMeta.Render("esc/q:close"),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return styles.DetailOverlay.Width(width).Render(strings.Join(lines, "\n"))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func helpRow(key, action string) string {
|
|
39
|
+
return styles.ColumnTitle.Render(key+": ") + styles.CardMeta.Render(action)
|
|
40
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
tea "github.com/charmbracelet/bubbletea"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func TestRenderHelp_containsTitle(t *testing.T) {
|
|
11
|
+
got := RenderHelp(60)
|
|
12
|
+
if !strings.Contains(got, "KEYBOARD SHORTCUTS") {
|
|
13
|
+
t.Error("RenderHelp missing title")
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func TestRenderHelp_containsShortcuts(t *testing.T) {
|
|
18
|
+
got := RenderHelp(60)
|
|
19
|
+
for _, shortcut := range []string{
|
|
20
|
+
"h / left",
|
|
21
|
+
"l / right",
|
|
22
|
+
"enter",
|
|
23
|
+
"e",
|
|
24
|
+
"r",
|
|
25
|
+
"up / k",
|
|
26
|
+
"down / j",
|
|
27
|
+
"?",
|
|
28
|
+
"esc / q",
|
|
29
|
+
"q / ctrl+c",
|
|
30
|
+
} {
|
|
31
|
+
if !strings.Contains(got, shortcut) {
|
|
32
|
+
t.Errorf("RenderHelp missing shortcut %q", shortcut)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func TestRenderHelp_containsCloseHint(t *testing.T) {
|
|
38
|
+
got := RenderHelp(60)
|
|
39
|
+
if !strings.Contains(got, "esc/q:close") {
|
|
40
|
+
t.Error("RenderHelp missing close hint")
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func TestUpdate_questionMarkOpensHelpOverlay(t *testing.T) {
|
|
45
|
+
m := NewModel(nil, "v1", "E04")
|
|
46
|
+
got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("?")})
|
|
47
|
+
updated := requireModel(t, got)
|
|
48
|
+
if updated.Overlay != OverlayHelp {
|
|
49
|
+
t.Errorf("Overlay = %q, want %q", updated.Overlay, OverlayHelp)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func TestUpdate_helpOverlayEscCloses(t *testing.T) {
|
|
54
|
+
m := NewModel(nil, "v1", "E04")
|
|
55
|
+
m.Overlay = OverlayHelp
|
|
56
|
+
got, _ := m.Update(tea.KeyMsg{Type: tea.KeyEsc})
|
|
57
|
+
updated := requireModel(t, got)
|
|
58
|
+
if updated.Overlay != OverlayNone {
|
|
59
|
+
t.Errorf("Overlay = %q after esc, want none", updated.Overlay)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
func TestUpdate_helpOverlayQCloses(t *testing.T) {
|
|
64
|
+
m := NewModel(nil, "v1", "E04")
|
|
65
|
+
m.Overlay = OverlayHelp
|
|
66
|
+
got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("q")})
|
|
67
|
+
updated := requireModel(t, got)
|
|
68
|
+
if updated.Overlay != OverlayNone {
|
|
69
|
+
t.Errorf("Overlay = %q after q, want none", updated.Overlay)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
func TestView_helpOverlayRendered(t *testing.T) {
|
|
74
|
+
m := NewModel(nil, "v1", "E04")
|
|
75
|
+
m.Width = 100
|
|
76
|
+
m.Height = 30
|
|
77
|
+
m.Overlay = OverlayHelp
|
|
78
|
+
got := m.View()
|
|
79
|
+
if !strings.Contains(got, "KEYBOARD SHORTCUTS") {
|
|
80
|
+
t.Error("View() with OverlayHelp missing help header")
|
|
81
|
+
}
|
|
82
|
+
if !strings.Contains(got, "q / ctrl+c") {
|
|
83
|
+
t.Error("View() with OverlayHelp missing quit shortcut")
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
const (
|
|
4
|
+
colOverhead = 4 // rounded border (1) + padding (1) each side
|
|
5
|
+
|
|
6
|
+
minColWidth = 10
|
|
7
|
+
|
|
8
|
+
epicPanelWidth = 28
|
|
9
|
+
epicPanelOverhead = 4
|
|
10
|
+
|
|
11
|
+
boardFrameOverhead = 4 // rounded border (2) + padding (2×1)
|
|
12
|
+
|
|
13
|
+
breakpointWide = 120
|
|
14
|
+
breakpointNarrow = 80
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
// Layout describes board geometry for a given terminal size.
|
|
18
|
+
type Layout struct {
|
|
19
|
+
EpicPanelVisible bool
|
|
20
|
+
EpicPanelWidth int
|
|
21
|
+
ColCount int
|
|
22
|
+
ColWidths []int
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// CalculateLayout returns the board layout for the given terminal dimensions.
|
|
26
|
+
//
|
|
27
|
+
// - >=120 cols: epic panel (28w) + 3 columns
|
|
28
|
+
// - 80–119 cols: 3 columns only
|
|
29
|
+
// - <80 cols: 1 column
|
|
30
|
+
func CalculateLayout(width, _ int) Layout {
|
|
31
|
+
inner := width - boardFrameOverhead
|
|
32
|
+
switch {
|
|
33
|
+
case width >= breakpointWide:
|
|
34
|
+
available := inner - (epicPanelWidth + epicPanelOverhead) - 3*colOverhead
|
|
35
|
+
cw := max(available/3, minColWidth)
|
|
36
|
+
return Layout{
|
|
37
|
+
EpicPanelVisible: true,
|
|
38
|
+
EpicPanelWidth: epicPanelWidth,
|
|
39
|
+
ColCount: 3,
|
|
40
|
+
ColWidths: []int{cw, cw, cw},
|
|
41
|
+
}
|
|
42
|
+
case width >= breakpointNarrow:
|
|
43
|
+
available := inner - 3*colOverhead
|
|
44
|
+
cw := max(available/3, minColWidth)
|
|
45
|
+
return Layout{
|
|
46
|
+
EpicPanelVisible: false,
|
|
47
|
+
ColCount: 3,
|
|
48
|
+
ColWidths: []int{cw, cw, cw},
|
|
49
|
+
}
|
|
50
|
+
default:
|
|
51
|
+
cw := max(inner-colOverhead, minColWidth)
|
|
52
|
+
return Layout{
|
|
53
|
+
EpicPanelVisible: false,
|
|
54
|
+
ColCount: 1,
|
|
55
|
+
ColWidths: []int{cw},
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import "testing"
|
|
4
|
+
|
|
5
|
+
func TestCalculateLayout_wide(t *testing.T) {
|
|
6
|
+
l := CalculateLayout(120, 40)
|
|
7
|
+
if !l.EpicPanelVisible {
|
|
8
|
+
t.Error("expected epic panel visible at width=120")
|
|
9
|
+
}
|
|
10
|
+
if l.EpicPanelWidth != epicPanelWidth {
|
|
11
|
+
t.Errorf("EpicPanelWidth = %d, want %d", l.EpicPanelWidth, epicPanelWidth)
|
|
12
|
+
}
|
|
13
|
+
if l.ColCount != 3 {
|
|
14
|
+
t.Errorf("ColCount = %d, want 3", l.ColCount)
|
|
15
|
+
}
|
|
16
|
+
if len(l.ColWidths) != 3 {
|
|
17
|
+
t.Fatalf("len(ColWidths) = %d, want 3", len(l.ColWidths))
|
|
18
|
+
}
|
|
19
|
+
// inner = 120-4 = 116; available = 116-(28+4)-3*4 = 72; cw = 72/3 = 24
|
|
20
|
+
for i, w := range l.ColWidths {
|
|
21
|
+
if w != 24 {
|
|
22
|
+
t.Errorf("ColWidths[%d] = %d, want 24", i, w)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func TestCalculateLayout_medium(t *testing.T) {
|
|
28
|
+
l := CalculateLayout(100, 40)
|
|
29
|
+
if l.EpicPanelVisible {
|
|
30
|
+
t.Error("epic panel should be hidden at width=100")
|
|
31
|
+
}
|
|
32
|
+
if l.ColCount != 3 {
|
|
33
|
+
t.Errorf("ColCount = %d, want 3", l.ColCount)
|
|
34
|
+
}
|
|
35
|
+
if len(l.ColWidths) != 3 {
|
|
36
|
+
t.Fatalf("len(ColWidths) = %d, want 3", len(l.ColWidths))
|
|
37
|
+
}
|
|
38
|
+
for i, w := range l.ColWidths {
|
|
39
|
+
if w != 28 {
|
|
40
|
+
t.Errorf("ColWidths[%d] = %d, want 28", i, w)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
func TestCalculateLayout_narrow(t *testing.T) {
|
|
46
|
+
l := CalculateLayout(60, 40)
|
|
47
|
+
if l.EpicPanelVisible {
|
|
48
|
+
t.Error("epic panel should be hidden at width=60")
|
|
49
|
+
}
|
|
50
|
+
if l.ColCount != 1 {
|
|
51
|
+
t.Errorf("ColCount = %d, want 1", l.ColCount)
|
|
52
|
+
}
|
|
53
|
+
// inner = 60 - 4 = 56; cw = 56 - 4 = 52
|
|
54
|
+
if l.ColWidths[0] != 52 {
|
|
55
|
+
t.Errorf("ColWidths[0] = %d, want 52", l.ColWidths[0])
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func TestCalculateLayout_tinyWidth_floorsAtMinColWidth(t *testing.T) {
|
|
60
|
+
l := CalculateLayout(4, 40)
|
|
61
|
+
if l.ColCount != 1 {
|
|
62
|
+
t.Errorf("ColCount = %d, want 1", l.ColCount)
|
|
63
|
+
}
|
|
64
|
+
if l.ColWidths[0] != minColWidth {
|
|
65
|
+
t.Errorf("ColWidths[0] = %d, want %d (minColWidth floor)", l.ColWidths[0], minColWidth)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func TestCalculateLayout_breakpointBoundaries(t *testing.T) {
|
|
70
|
+
cases := []struct {
|
|
71
|
+
width int
|
|
72
|
+
wantColCount int
|
|
73
|
+
wantEpic bool
|
|
74
|
+
}{
|
|
75
|
+
{119, 3, false},
|
|
76
|
+
{120, 3, true},
|
|
77
|
+
{79, 1, false},
|
|
78
|
+
{80, 3, false},
|
|
79
|
+
}
|
|
80
|
+
for _, tc := range cases {
|
|
81
|
+
l := CalculateLayout(tc.width, 40)
|
|
82
|
+
if l.ColCount != tc.wantColCount {
|
|
83
|
+
t.Errorf("width=%d: ColCount = %d, want %d", tc.width, l.ColCount, tc.wantColCount)
|
|
84
|
+
}
|
|
85
|
+
if l.EpicPanelVisible != tc.wantEpic {
|
|
86
|
+
t.Errorf("width=%d: EpicPanelVisible = %v, want %v", tc.width, l.EpicPanelVisible, tc.wantEpic)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
|
|
7
|
+
tea "github.com/charmbracelet/bubbletea"
|
|
8
|
+
"github.com/opencode/savepoint/internal/data"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
type OverlayType string
|
|
12
|
+
|
|
13
|
+
const (
|
|
14
|
+
OverlayNone OverlayType = ""
|
|
15
|
+
OverlayHelp OverlayType = "help"
|
|
16
|
+
OverlayEpic OverlayType = "epic"
|
|
17
|
+
OverlayRelease OverlayType = "release"
|
|
18
|
+
OverlayDetail OverlayType = "detail"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
// Model holds all board state. Tasks are grouped by column for O(1) column access.
|
|
22
|
+
type Model struct {
|
|
23
|
+
AllTasks []data.Task
|
|
24
|
+
Tasks map[data.ColumnType][]data.Task
|
|
25
|
+
FocusedColumn data.ColumnType
|
|
26
|
+
FocusedTask int
|
|
27
|
+
SelectedEpic string
|
|
28
|
+
SelectedRelease string
|
|
29
|
+
Epics []string
|
|
30
|
+
EpicCursor int
|
|
31
|
+
Releases []string
|
|
32
|
+
ReleaseEpics map[string][]string
|
|
33
|
+
ReleaseCursor int
|
|
34
|
+
Overlay OverlayType
|
|
35
|
+
Width int
|
|
36
|
+
Height int
|
|
37
|
+
StatusMessage string
|
|
38
|
+
Root string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// NewModel groups tasks by column and returns an initialized Model.
|
|
42
|
+
func NewModel(tasks []data.Task, release, epic string) Model {
|
|
43
|
+
m := Model{
|
|
44
|
+
AllTasks: append([]data.Task(nil), tasks...),
|
|
45
|
+
FocusedColumn: data.ColumnPlanned,
|
|
46
|
+
FocusedTask: 0,
|
|
47
|
+
SelectedEpic: epic,
|
|
48
|
+
SelectedRelease: release,
|
|
49
|
+
Overlay: OverlayNone,
|
|
50
|
+
}
|
|
51
|
+
m.refreshTasks()
|
|
52
|
+
return m
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func (m Model) Init() tea.Cmd {
|
|
56
|
+
return tea.Batch()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func groupedTasks(tasks []data.Task) map[data.ColumnType][]data.Task {
|
|
60
|
+
grouped := map[data.ColumnType][]data.Task{
|
|
61
|
+
data.ColumnPlanned: {},
|
|
62
|
+
data.ColumnInProgress: {},
|
|
63
|
+
data.ColumnDone: {},
|
|
64
|
+
}
|
|
65
|
+
for _, t := range tasks {
|
|
66
|
+
col := t.Column
|
|
67
|
+
if col == "" {
|
|
68
|
+
col = data.ColumnPlanned
|
|
69
|
+
}
|
|
70
|
+
grouped[col] = append(grouped[col], t)
|
|
71
|
+
}
|
|
72
|
+
return grouped
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func (m *Model) refreshTasks() {
|
|
76
|
+
visible := make([]data.Task, 0, len(m.AllTasks))
|
|
77
|
+
for _, t := range m.AllTasks {
|
|
78
|
+
if m.SelectedRelease != "" && t.Release != "" && t.Release != m.SelectedRelease {
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
if m.SelectedEpic != "" && t.Epic != "" && t.Epic != m.SelectedEpic {
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
visible = append(visible, t)
|
|
85
|
+
}
|
|
86
|
+
m.Tasks = groupedTasks(visible)
|
|
87
|
+
m.clampFocusedTask()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func (m *Model) refreshEpicsForRelease() {
|
|
91
|
+
if len(m.ReleaseEpics) == 0 {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
epics := m.ReleaseEpics[m.SelectedRelease]
|
|
96
|
+
m.Epics = append([]string(nil), epics...)
|
|
97
|
+
if len(m.Epics) == 0 {
|
|
98
|
+
m.SelectedEpic = ""
|
|
99
|
+
m.EpicCursor = 0
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for _, epic := range m.Epics {
|
|
104
|
+
if epic == m.SelectedEpic {
|
|
105
|
+
m.EpicCursor = epicIndex(m.Epics, m.SelectedEpic)
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
m.SelectedEpic = m.Epics[0]
|
|
111
|
+
m.EpicCursor = 0
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
func (m *Model) clampFocusedTask() {
|
|
115
|
+
tasks := m.Tasks[m.FocusedColumn]
|
|
116
|
+
if len(tasks) == 0 {
|
|
117
|
+
m.FocusedTask = 0
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
if m.FocusedTask >= len(tasks) {
|
|
121
|
+
m.FocusedTask = len(tasks) - 1
|
|
122
|
+
}
|
|
123
|
+
if m.FocusedTask < 0 {
|
|
124
|
+
m.FocusedTask = 0
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
func (m *Model) writeRouterReleaseEpic() error {
|
|
129
|
+
routerPath := filepath.Join(m.Root, "router.md")
|
|
130
|
+
|
|
131
|
+
fi, err := os.Stat(routerPath)
|
|
132
|
+
if err != nil {
|
|
133
|
+
return err
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
content, err := os.ReadFile(routerPath)
|
|
137
|
+
if err != nil {
|
|
138
|
+
return err
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
r := data.NewRouterReader()
|
|
142
|
+
state, err := r.ReadState(string(content))
|
|
143
|
+
if err != nil {
|
|
144
|
+
return err
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
state.Epic = m.SelectedEpic
|
|
148
|
+
state.Release = m.SelectedRelease
|
|
149
|
+
|
|
150
|
+
return data.WriteRouterState(m.Root, state, fi.ModTime())
|
|
151
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"testing"
|
|
5
|
+
|
|
6
|
+
"github.com/opencode/savepoint/internal/data"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
func TestNewModel_emptyTasks(t *testing.T) {
|
|
10
|
+
m := NewModel(nil, "v1", "E03")
|
|
11
|
+
|
|
12
|
+
if m.SelectedRelease != "v1" {
|
|
13
|
+
t.Errorf("SelectedRelease = %q, want %q", m.SelectedRelease, "v1")
|
|
14
|
+
}
|
|
15
|
+
if m.SelectedEpic != "E03" {
|
|
16
|
+
t.Errorf("SelectedEpic = %q, want %q", m.SelectedEpic, "E03")
|
|
17
|
+
}
|
|
18
|
+
if m.FocusedColumn != data.ColumnPlanned {
|
|
19
|
+
t.Errorf("FocusedColumn = %q, want %q", m.FocusedColumn, data.ColumnPlanned)
|
|
20
|
+
}
|
|
21
|
+
if m.FocusedTask != 0 {
|
|
22
|
+
t.Errorf("FocusedTask = %d, want 0", m.FocusedTask)
|
|
23
|
+
}
|
|
24
|
+
if m.Overlay != OverlayNone {
|
|
25
|
+
t.Errorf("Overlay = %q, want empty", m.Overlay)
|
|
26
|
+
}
|
|
27
|
+
for _, col := range []data.ColumnType{data.ColumnPlanned, data.ColumnInProgress, data.ColumnDone} {
|
|
28
|
+
if _, ok := m.Tasks[col]; !ok {
|
|
29
|
+
t.Errorf("Tasks missing column %q", col)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func TestNewModel_groupsByColumn(t *testing.T) {
|
|
35
|
+
tasks := []data.Task{
|
|
36
|
+
{ID: "T1", Column: data.ColumnPlanned},
|
|
37
|
+
{ID: "T2", Column: data.ColumnInProgress},
|
|
38
|
+
{ID: "T3", Column: data.ColumnDone},
|
|
39
|
+
{ID: "T4", Column: data.ColumnPlanned},
|
|
40
|
+
}
|
|
41
|
+
m := NewModel(tasks, "v1", "E03")
|
|
42
|
+
|
|
43
|
+
if got := len(m.Tasks[data.ColumnPlanned]); got != 2 {
|
|
44
|
+
t.Errorf("Planned count = %d, want 2", got)
|
|
45
|
+
}
|
|
46
|
+
if got := len(m.Tasks[data.ColumnInProgress]); got != 1 {
|
|
47
|
+
t.Errorf("InProgress count = %d, want 1", got)
|
|
48
|
+
}
|
|
49
|
+
if got := len(m.Tasks[data.ColumnDone]); got != 1 {
|
|
50
|
+
t.Errorf("Done count = %d, want 1", got)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func TestNewModel_emptyColumnDefaultsToPlanned(t *testing.T) {
|
|
55
|
+
tasks := []data.Task{{ID: "T1"}}
|
|
56
|
+
m := NewModel(tasks, "v1", "E03")
|
|
57
|
+
|
|
58
|
+
if got := len(m.Tasks[data.ColumnPlanned]); got != 1 {
|
|
59
|
+
t.Errorf("Planned count = %d, want 1 (empty column defaulted)", got)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
func TestModel_Init(t *testing.T) {
|
|
64
|
+
m := NewModel(nil, "v1", "E03")
|
|
65
|
+
// Init must not panic and returns a valid Cmd (nil or batch).
|
|
66
|
+
_ = m.Init()
|
|
67
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
|
|
6
|
+
"github.com/opencode/savepoint/internal/styles"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
const releaseActiveMarker = "►"
|
|
10
|
+
|
|
11
|
+
// RenderReleaseDropdown renders the release selection dropdown overlay.
|
|
12
|
+
func RenderReleaseDropdown(releases []string, cursor int, width int) string {
|
|
13
|
+
inner := width - epicPanelOverhead
|
|
14
|
+
if inner < 2 {
|
|
15
|
+
inner = 2
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
lines := []string{styles.ColumnTitleFocused.Render("SELECT RELEASE"), strings.Repeat("─", inner)}
|
|
19
|
+
for i, r := range releases {
|
|
20
|
+
label := truncate(r, inner-2)
|
|
21
|
+
if i == cursor {
|
|
22
|
+
lines = append(lines, styles.TaskItemFocused.Render(releaseActiveMarker+" "+label))
|
|
23
|
+
} else {
|
|
24
|
+
lines = append(lines, styles.TaskItem.Render(" "+label))
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if len(releases) == 0 {
|
|
28
|
+
lines = append(lines, styles.TaskItem.Render("(none)"))
|
|
29
|
+
}
|
|
30
|
+
lines = append(lines, "", styles.CardMeta.Render("↑↓:nav enter:select esc:cancel"))
|
|
31
|
+
return styles.EpicPanel.Width(width).Render(strings.Join(lines, "\n"))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// releaseIndex returns the index of selected in releases, or 0 if not found.
|
|
35
|
+
func releaseIndex(releases []string, selected string) int {
|
|
36
|
+
for i, r := range releases {
|
|
37
|
+
if r == selected {
|
|
38
|
+
return i
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return 0
|
|
42
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
tea "github.com/charmbracelet/bubbletea"
|
|
8
|
+
"github.com/opencode/savepoint/internal/data"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
func TestRenderReleaseDropdown_showsReleases(t *testing.T) {
|
|
12
|
+
releases := []string{"v1", "v2", "v3"}
|
|
13
|
+
out := RenderReleaseDropdown(releases, 0, 40)
|
|
14
|
+
for _, r := range releases {
|
|
15
|
+
if !strings.Contains(out, r) {
|
|
16
|
+
t.Errorf("output missing release %q", r)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func TestRenderReleaseDropdown_marksCurrentCursor(t *testing.T) {
|
|
22
|
+
releases := []string{"v1", "v2"}
|
|
23
|
+
out := RenderReleaseDropdown(releases, 1, 40)
|
|
24
|
+
if !strings.Contains(out, releaseActiveMarker) {
|
|
25
|
+
t.Error("output missing active marker")
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func TestRenderReleaseDropdown_emptyList(t *testing.T) {
|
|
30
|
+
out := RenderReleaseDropdown(nil, 0, 40)
|
|
31
|
+
if !strings.Contains(out, "(none)") {
|
|
32
|
+
t.Error("expected '(none)' for empty releases")
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func TestRenderReleaseDropdown_hintsPresent(t *testing.T) {
|
|
37
|
+
out := RenderReleaseDropdown([]string{"v1"}, 0, 40)
|
|
38
|
+
if !strings.Contains(out, "esc:cancel") {
|
|
39
|
+
t.Error("expected hint text in dropdown")
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func TestReleaseIndex_found(t *testing.T) {
|
|
44
|
+
releases := []string{"v1", "v2", "v3"}
|
|
45
|
+
if got := releaseIndex(releases, "v2"); got != 1 {
|
|
46
|
+
t.Errorf("releaseIndex = %d, want 1", got)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func TestReleaseIndex_notFound(t *testing.T) {
|
|
51
|
+
releases := []string{"v1", "v2"}
|
|
52
|
+
if got := releaseIndex(releases, "v9"); got != 0 {
|
|
53
|
+
t.Errorf("releaseIndex = %d, want 0", got)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func TestUpdate_rOpensReleaseOverlay(t *testing.T) {
|
|
58
|
+
m := NewModel(nil, "v1", "E03")
|
|
59
|
+
m.Releases = []string{"v1", "v2"}
|
|
60
|
+
got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("r")})
|
|
61
|
+
updated := requireModel(t, got)
|
|
62
|
+
if updated.Overlay != OverlayRelease {
|
|
63
|
+
t.Errorf("Overlay = %q, want %q", updated.Overlay, OverlayRelease)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func TestUpdate_rSetsCursorToCurrentRelease(t *testing.T) {
|
|
68
|
+
m := NewModel(nil, "v2", "E03")
|
|
69
|
+
m.Releases = []string{"v1", "v2", "v3"}
|
|
70
|
+
got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("r")})
|
|
71
|
+
updated := requireModel(t, got)
|
|
72
|
+
if updated.ReleaseCursor != 1 {
|
|
73
|
+
t.Errorf("ReleaseCursor = %d, want 1", updated.ReleaseCursor)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
func TestUpdate_escClosesReleaseOverlay(t *testing.T) {
|
|
78
|
+
m := NewModel(nil, "v1", "E03")
|
|
79
|
+
m.Overlay = OverlayRelease
|
|
80
|
+
got, _ := m.Update(tea.KeyMsg{Type: tea.KeyEsc})
|
|
81
|
+
updated := requireModel(t, got)
|
|
82
|
+
if updated.Overlay != OverlayNone {
|
|
83
|
+
t.Errorf("Overlay = %q, want empty", updated.Overlay)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func TestUpdate_releaseNavDown(t *testing.T) {
|
|
88
|
+
m := NewModel(nil, "v1", "E03")
|
|
89
|
+
m.Releases = []string{"v1", "v2"}
|
|
90
|
+
m.Overlay = OverlayRelease
|
|
91
|
+
m.ReleaseCursor = 0
|
|
92
|
+
got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("j")})
|
|
93
|
+
updated := requireModel(t, got)
|
|
94
|
+
if updated.ReleaseCursor != 1 {
|
|
95
|
+
t.Errorf("ReleaseCursor = %d, want 1", updated.ReleaseCursor)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
func TestUpdate_releaseNavUp(t *testing.T) {
|
|
100
|
+
m := NewModel(nil, "v1", "E03")
|
|
101
|
+
m.Releases = []string{"v1", "v2"}
|
|
102
|
+
m.Overlay = OverlayRelease
|
|
103
|
+
m.ReleaseCursor = 1
|
|
104
|
+
got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("k")})
|
|
105
|
+
updated := requireModel(t, got)
|
|
106
|
+
if updated.ReleaseCursor != 0 {
|
|
107
|
+
t.Errorf("ReleaseCursor = %d, want 0", updated.ReleaseCursor)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func TestUpdate_releaseEnterSelects(t *testing.T) {
|
|
112
|
+
tasks := []data.Task{
|
|
113
|
+
{ID: "T1", Epic: "E03", Release: "v1", Column: data.ColumnPlanned},
|
|
114
|
+
{ID: "T2", Epic: "E03", Release: "v2", Column: data.ColumnPlanned},
|
|
115
|
+
}
|
|
116
|
+
m := NewModel(tasks, "v1", "E03")
|
|
117
|
+
m.Releases = []string{"v1", "v2"}
|
|
118
|
+
m.Overlay = OverlayRelease
|
|
119
|
+
m.ReleaseCursor = 1
|
|
120
|
+
got, _ := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
|
|
121
|
+
updated := requireModel(t, got)
|
|
122
|
+
if updated.SelectedRelease != "v2" {
|
|
123
|
+
t.Errorf("SelectedRelease = %q, want %q", updated.SelectedRelease, "v2")
|
|
124
|
+
}
|
|
125
|
+
if updated.Overlay != OverlayNone {
|
|
126
|
+
t.Errorf("Overlay = %q, want empty after selection", updated.Overlay)
|
|
127
|
+
}
|
|
128
|
+
if got := len(updated.Tasks[data.ColumnPlanned]); got != 1 {
|
|
129
|
+
t.Errorf("planned task count = %d, want 1 after release selection", got)
|
|
130
|
+
}
|
|
131
|
+
if updated.Tasks[data.ColumnPlanned][0].ID != "T2" {
|
|
132
|
+
t.Errorf("visible task = %q, want T2", updated.Tasks[data.ColumnPlanned][0].ID)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
func TestUpdate_releaseEnterRefreshesEpics(t *testing.T) {
|
|
137
|
+
tasks := []data.Task{
|
|
138
|
+
{ID: "T1", Epic: "E01", Release: "v1", Column: data.ColumnPlanned},
|
|
139
|
+
{ID: "T2", Epic: "E03", Release: "v2", Column: data.ColumnPlanned},
|
|
140
|
+
}
|
|
141
|
+
m := NewModel(tasks, "v1", "E01")
|
|
142
|
+
m.Releases = []string{"v1", "v2"}
|
|
143
|
+
m.ReleaseEpics = map[string][]string{
|
|
144
|
+
"v1": []string{"E01"},
|
|
145
|
+
"v2": []string{"E03"},
|
|
146
|
+
}
|
|
147
|
+
m.Overlay = OverlayRelease
|
|
148
|
+
m.ReleaseCursor = 1
|
|
149
|
+
|
|
150
|
+
got, _ := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
|
|
151
|
+
updated := requireModel(t, got)
|
|
152
|
+
|
|
153
|
+
if updated.SelectedEpic != "E03" {
|
|
154
|
+
t.Errorf("SelectedEpic = %q, want E03", updated.SelectedEpic)
|
|
155
|
+
}
|
|
156
|
+
if len(updated.Epics) != 1 || updated.Epics[0] != "E03" {
|
|
157
|
+
t.Errorf("Epics = %v, want [E03]", updated.Epics)
|
|
158
|
+
}
|
|
159
|
+
if got := len(updated.Tasks[data.ColumnPlanned]); got != 1 {
|
|
160
|
+
t.Fatalf("planned task count = %d, want 1", got)
|
|
161
|
+
}
|
|
162
|
+
if updated.Tasks[data.ColumnPlanned][0].ID != "T2" {
|
|
163
|
+
t.Errorf("visible task = %q, want T2", updated.Tasks[data.ColumnPlanned][0].ID)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
func TestView_releaseDropdownKeepsBoardBehind(t *testing.T) {
|
|
168
|
+
m := NewModel(nil, "v1", "E03")
|
|
169
|
+
m.Width = 100
|
|
170
|
+
m.Height = 24
|
|
171
|
+
m.Overlay = OverlayRelease
|
|
172
|
+
m.Releases = []string{"v1", "v2"}
|
|
173
|
+
got := m.View()
|
|
174
|
+
if !strings.Contains(got, "S A V E P O I N T") {
|
|
175
|
+
t.Error("View() with OverlayRelease should keep board visible behind overlay")
|
|
176
|
+
}
|
|
177
|
+
}
|