savepoint 1.0.1 → 1.0.3
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 +15 -1
- package/.golangci.yml +11 -0
- package/.savepoint/Design.md +52 -46
- package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T001-init-module.md +1 -1
- package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T005-layout.md +1 -1
- package/.savepoint/releases/v1/epics/E04-board-components/tasks/T002-card.md +1 -1
- package/.savepoint/releases/v1/epics/E04-board-components/tasks/T006-help-overlay.md +1 -1
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/{Design.md → E06-Detail.md} +5 -3
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T002-header-and-dividers.md +1 -1
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T003-footer-status-bar.md +1 -1
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T004-component-refinement.md +1 -1
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T010-auto-refresh-watcher.md +2 -0
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/{Design.md → E01-Detail.md} +9 -1
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/{T007-next-activity-header.md → T001-next-activity-header.md} +13 -12
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T002-rename-epic-design-files.md +9 -9
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T003-rename-release-prd.md +2 -2
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T004-update-instruction-files.md +13 -12
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T005-update-cross-references.md +14 -13
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T006-column-and-detail-scrolling.md +25 -15
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T007-column-focus-border-stability.md +57 -0
- package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/E02-Audit.md +124 -0
- package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/{Design.md → E02-Detail.md} +12 -3
- package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T001-fix-makefile.md +11 -8
- package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T002-linux-build-target.md +12 -7
- package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T003-macos-build-target.md +9 -5
- package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T004-smoke-tests-and-artifacts.md +30 -9
- package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/E03-Audit.md +195 -0
- package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/E03-Detail.md +45 -0
- package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T001-border-resize-fix.md +40 -0
- package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T002-next-activity-below-header.md +64 -0
- package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T003-checkbox-rendering-fix.md +56 -0
- package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T005-unify-status-glyphs.md +65 -0
- package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T006-forced-256-color-profile.md +36 -0
- package/.savepoint/releases/v1.1/epics/E04-epic-navigation/E04-Audit.md +167 -0
- package/.savepoint/releases/v1.1/epics/E04-epic-navigation/E04-Detail.md +51 -0
- package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T001-sidebar-focusable-navigation.md +65 -0
- package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T002-epic-detail-overlay.md +73 -0
- package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T003-epic-status-glyphs.md +73 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/E05-Audit.md +237 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/E05-Detail.md +54 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T001-update-agents-md.md +45 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T002-update-router-md.md +40 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T003-update-design-md.md +47 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T004-implement-m-hotkey.md +98 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T005-update-help-overlay.md +33 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T006-tests-and-quality-gates.md +62 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/E06-Audit.md +56 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/E06-Detail.md +63 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T005-proposals.md +44 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T007-apply-close.md +35 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T009-integration.md +40 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T010-audit-file-migration.md +45 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T011-model-tab-state.md +26 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T012-epic-audit-render.md +33 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T013-handle-tab-keys.md +34 -0
- package/.savepoint/releases/v1.1/epics/E06-audit-command/tasks/T014-tab-indicator.md +33 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/E07-Audit.md +336 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/E07-Detail.md +61 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T001-cli-entrypoint.md +37 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T002-target-validation.md +28 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T003-scaffold-writer.md +46 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T004-atomic-writes.md +27 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T005-magic-prompt.md +25 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T006-clipboard.md +26 -0
- package/.savepoint/releases/v1.1/epics/E07-init-command/tasks/T007-integration-test.md +26 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/E08-Audit.md +333 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/E08-Detail.md +68 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T001-cli-entrypoint.md +26 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T002-non-tty-fallback.md +27 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T003-tui-app-shell.md +28 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T004-board-model.md +29 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T005-detail-pane.md +27 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T006-status-transitions.md +29 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T007-theme-fallbacks.md +29 -0
- package/.savepoint/releases/v1.1/epics/E08-board-command/tasks/T008-integration-test.md +27 -0
- package/.savepoint/releases/v1.1/epics/E09-doctor-command/E09-Audit.md +207 -0
- package/.savepoint/releases/v1.1/epics/E09-doctor-command/E09-Detail.md +65 -0
- package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T001-cli-entrypoint.md +24 -0
- package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T002-config-router-validation.md +28 -0
- package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T003-structure-checks.md +29 -0
- package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T004-dependency-checks.md +27 -0
- package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T005-audit-orphan-checks.md +28 -0
- package/.savepoint/releases/v1.1/epics/E09-doctor-command/tasks/T006-quality-gates-report.md +31 -0
- package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/E11-Detail.md +36 -0
- package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T001-debug-logging.md +25 -0
- package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T002-increase-debounce.md +21 -0
- package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T003-error-handling.md +22 -0
- package/.savepoint/releases/v1.1/epics/E11-board-refresh-fix/tasks/T004-test-verify.md +29 -0
- package/.savepoint/releases/v1.1/epics/E12-validation-fix/E12-Audit.md +444 -0
- package/.savepoint/releases/v1.1/epics/E12-validation-fix/E12-Detail.md +45 -0
- package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T001-default-phase.md +35 -0
- package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T002-default-status.md +19 -0
- package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T003-better-errors.md +29 -0
- package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T004-validate-on-write.md +25 -0
- package/.savepoint/releases/v1.1/epics/E12-validation-fix/tasks/T005-tests.md +37 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/E13-Audit.md +118 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/E13-Detail.md +73 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T001-safe-cleanup.md +66 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T002-bug-fixes.md +35 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T003-centralize-duplication.md +60 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T004-infrastructure.md +33 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T005-decompose-update.md +37 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T006-async-io.md +40 -0
- package/.savepoint/releases/v1.1/epics/E13-audit-remediation/tasks/T007-test-coverage.md +37 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/E14-Audit.md +267 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/E14-Detail.md +54 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T001-group-model.md +39 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T002-data-interfaces.md +42 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T003-discover-orphans.md +33 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T004-epic-panel-headings.md +35 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T005-shell-tokenization.md +27 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T006-unify-enums.md +29 -0
- package/.savepoint/releases/v1.1/epics/E14-structural-improvements/tasks/T007-testutil-package.md +28 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/E15-Detail.md +43 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T001-benchmarks.md +31 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T002-fuzz-targets.md +28 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T003-debug-flag.md +30 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T004-dist-checksums.md +27 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T005-windows-targets.md +28 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T006-abbreviation-splitting.md +26 -0
- package/.savepoint/releases/v1.1/epics/E15-hardening/tasks/T007-root-test-allowlist.md +28 -0
- package/.savepoint/releases/v1.1/epics/_archived/T001-cli-entrypoint.md +25 -0
- package/.savepoint/releases/v1.1/epics/_archived/T002-quality-gates.md +27 -0
- package/.savepoint/releases/v1.1/epics/_archived/T003-snapshot.md +27 -0
- package/.savepoint/releases/v1.1/epics/_archived/T004-ai-reconcile.md +29 -0
- package/.savepoint/releases/v1.1/epics/_archived/T006-tui-review.md +31 -0
- package/.savepoint/releases/v1.1/epics/_archived/T008-skip-handling.md +34 -0
- package/.savepoint/releases/v1.1/v1.1-PRD.md +139 -0
- package/.savepoint/router.md +29 -108
- package/AGENTS.md +69 -111
- package/Makefile +19 -3
- package/README.md +6 -6
- package/agent-skills/savepoint-audit/SKILL.md +87 -35
- package/agent-skills/savepoint-build-task/SKILL.md +9 -4
- package/agent-skills/savepoint-create-plan/SKILL.md +10 -5
- package/agent-skills/savepoint-create-task/SKILL.md +44 -31
- package/agent-skills/savepoint-draft-prd/SKILL.md +8 -3
- package/agent-skills/savepoint-system-design/SKILL.md +8 -3
- package/agent_skills_test.go +91 -0
- package/cmd/board.go +59 -0
- package/cmd/board_test.go +137 -0
- package/cmd/doctor.go +53 -0
- package/cmd/doctor_test.go +146 -0
- package/cmd/init.go +63 -0
- package/cmd/init_test.go +104 -0
- package/internal/board/board.go +69 -49
- package/internal/board/board_test.go +83 -67
- package/internal/board/card.go +71 -20
- package/internal/board/card_test.go +141 -12
- package/internal/board/column.go +77 -11
- package/internal/board/column_test.go +63 -13
- package/internal/board/detail.go +107 -72
- package/internal/board/detail_test.go +117 -26
- package/internal/board/epic_panel.go +211 -18
- package/internal/board/epic_panel_test.go +637 -14
- package/internal/board/help.go +1 -0
- package/internal/board/help_test.go +1 -0
- package/internal/board/integration_test.go +266 -0
- package/internal/board/interfaces.go +65 -0
- package/internal/board/interfaces_test.go +114 -0
- package/internal/board/io.go +93 -0
- package/internal/board/layout.go +12 -2
- package/internal/board/layout_test.go +17 -0
- package/internal/board/model.go +130 -52
- package/internal/board/plain.go +88 -0
- package/internal/board/plain_test.go +117 -0
- package/internal/board/release.go +1 -9
- package/internal/board/release_test.go +6 -6
- package/internal/board/render_policy_test.go +77 -0
- package/internal/board/status.go +23 -0
- package/internal/board/theme.go +24 -0
- package/internal/board/theme_test.go +31 -0
- package/internal/board/transitions.go +113 -88
- package/internal/board/transitions_test.go +164 -141
- package/internal/board/tui.go +32 -0
- package/internal/board/update.go +472 -94
- package/internal/board/update_test.go +447 -0
- package/internal/board/util.go +76 -0
- package/internal/board/view.go +139 -22
- package/internal/board/view_test.go +171 -3
- package/internal/board/watch.go +57 -9
- package/internal/buildtool/main.go +211 -0
- package/internal/buildtool/main_test.go +46 -0
- package/internal/data/config.go +17 -3
- package/internal/data/config_test.go +49 -0
- package/internal/data/discover.go +26 -0
- package/internal/data/discover_test.go +34 -10
- package/internal/data/errors.go +4 -0
- package/internal/data/lifecycle.go +13 -6
- package/internal/data/lifecycle_test.go +14 -11
- package/internal/data/parser.go +29 -6
- package/internal/data/parser_test.go +66 -7
- package/internal/data/task.go +1 -0
- package/internal/data/write.go +85 -11
- package/internal/data/write_test.go +167 -0
- package/internal/doctor/checks.go +567 -0
- package/internal/doctor/checks_test.go +716 -0
- package/internal/doctor/gates.go +193 -0
- package/internal/doctor/gates_test.go +166 -0
- package/internal/doctor/interfaces.go +64 -0
- package/internal/doctor/interfaces_test.go +104 -0
- package/internal/doctor/repairs.go +80 -0
- package/internal/doctor/repairs_test.go +81 -0
- package/internal/doctor/report.go +157 -0
- package/internal/doctor/report_test.go +89 -0
- package/internal/init/clipboard.go +146 -0
- package/internal/init/clipboard_test.go +74 -0
- package/internal/init/install.go +16 -0
- package/internal/init/integration_test.go +197 -0
- package/internal/init/prompt.go +14 -0
- package/internal/init/prompt_test.go +77 -0
- package/internal/init/scaffold.go +59 -0
- package/internal/init/scaffold_test.go +179 -0
- package/internal/init/template_freshness_test.go +56 -0
- package/internal/init/validate.go +85 -0
- package/internal/init/validate_test.go +141 -0
- package/internal/init/write.go +73 -0
- package/internal/init/write_test.go +91 -0
- package/internal/styles/palette.go +3 -3
- package/internal/styles/styles.go +39 -12
- package/internal/styles/styles_test.go +133 -0
- package/internal/testutil/fixture.go +113 -0
- package/internal/testutil/fs.go +26 -0
- package/main.go +107 -1
- package/package.json +2 -2
- package/project-audit/audit_report_glm_5.1.md +411 -0
- package/project-audit/audit_report_opus_4.6 +406 -0
- package/project-audit/consolidated-audit-report.md +456 -0
- package/savepoint +0 -0
- package/templates/project/.savepoint/Design.md +2 -2
- package/templates/project/.savepoint/router.md +15 -14
- package/templates/project/AGENTS.md +56 -98
- package/templates/project/agent-skills/savepoint-audit/SKILL.md +87 -0
- package/templates/project/agent-skills/savepoint-build-task/SKILL.md +44 -0
- package/templates/project/agent-skills/savepoint-create-plan/SKILL.md +33 -0
- package/templates/project/agent-skills/savepoint-create-task/SKILL.md +44 -0
- package/templates/project/agent-skills/savepoint-draft-prd/SKILL.md +37 -0
- package/templates/project/agent-skills/savepoint-system-design/SKILL.md +38 -0
- package/templates/prompts/audit-reconciliation.prompt.md +35 -30
- package/templates/prompts/design.prompt.md +3 -1
- package/templates/prompts/epic-design.prompt.md +3 -3
- package/templates/prompts/task-breakdown.prompt.md +1 -1
- package/templates/prompts/task-building.prompt.md +1 -1
- package/templates/prompts/task-planning.prompt.md +1 -1
- package/.savepoint/audit/E01-go-setup/proposals.md +0 -166
- package/.savepoint/audit/E01-go-setup/snapshot.md +0 -71
- package/.savepoint/audit/E01-scaffolding/proposals/AGENTS.md +0 -66
- package/.savepoint/audit/E01-scaffolding/proposals/Design.md +0 -210
- package/.savepoint/audit/E01-scaffolding/proposals/epic-Design.md +0 -117
- package/.savepoint/audit/E01-scaffolding/proposals/quality-review.md +0 -101
- package/.savepoint/audit/E01-scaffolding/snapshot.md +0 -54
- package/.savepoint/audit/E02-data-model/snapshot.md +0 -128
- package/.savepoint/audit/E02-data-readers/proposals.md +0 -123
- package/.savepoint/audit/E02-data-readers/snapshot.md +0 -54
- package/.savepoint/audit/E03-board-tui-core/proposals.md +0 -146
- package/.savepoint/audit/E03-board-tui-core/snapshot.md +0 -57
- package/.savepoint/audit/E03-cli-foundation/snapshot.md +0 -106
- package/.savepoint/audit/E04-board-components/proposals.md +0 -118
- package/.savepoint/audit/E04-board-components/snapshot.md +0 -77
- package/.savepoint/audit/E04-templates-and-prompts/snapshot.md +0 -115
- package/.savepoint/audit/E05-init-command/snapshot.md +0 -125
- package/.savepoint/audit/E05-phase-transitions/proposals.md +0 -83
- package/.savepoint/audit/E05-phase-transitions/snapshot.md +0 -36
- package/.savepoint/audit/E06-atari-noir-layout/proposals.md +0 -130
- package/.savepoint/audit/E06-atari-noir-layout/snapshot.md +0 -84
- package/.savepoint/audit/E06-tui-board/snapshot.md +0 -64
- package/.savepoint/audit/E07-audit-pipeline/snapshot.md +0 -165
- package/.savepoint/audit/E08-board-workflow-cleanup/snapshot.md +0 -65
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T001-border-resize-fix.md +0 -36
- package/ink-cli-ui-design.zip +0 -0
- package/main.exe +0 -0
- package/savepoint.exe +0 -0
- /package/.savepoint/releases/v1/epics/E01-go-setup/{Design.md → E01-Detail.md} +0 -0
- /package/.savepoint/releases/v1/epics/E02-data-readers/{Design.md → E02-Detail.md} +0 -0
- /package/.savepoint/releases/v1/epics/E03-board-tui-core/{Design.md → E03-Detail.md} +0 -0
- /package/.savepoint/releases/v1/epics/E04-board-components/{Design.md → E04-Detail.md} +0 -0
- /package/.savepoint/releases/v1/epics/E05-phase-transitions/{Design.md → E05-Detail.md} +0 -0
- /package/.savepoint/releases/v1/{PRD.md → v1-PRD.md} +0 -0
package/internal/board/model.go
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
package board
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
-
"os"
|
|
5
|
-
"path/filepath"
|
|
6
|
-
|
|
7
4
|
tea "github.com/charmbracelet/bubbletea"
|
|
8
5
|
"github.com/fsnotify/fsnotify"
|
|
9
6
|
"github.com/opencode/savepoint/internal/data"
|
|
@@ -12,44 +9,102 @@ import (
|
|
|
12
9
|
type OverlayType string
|
|
13
10
|
|
|
14
11
|
const (
|
|
15
|
-
OverlayNone
|
|
16
|
-
OverlayHelp
|
|
17
|
-
OverlayEpic
|
|
18
|
-
OverlayRelease
|
|
19
|
-
OverlayDetail
|
|
12
|
+
OverlayNone OverlayType = ""
|
|
13
|
+
OverlayHelp OverlayType = "help"
|
|
14
|
+
OverlayEpic OverlayType = "epic"
|
|
15
|
+
OverlayRelease OverlayType = "release"
|
|
16
|
+
OverlayDetail OverlayType = "detail"
|
|
17
|
+
OverlayEpicDetail OverlayType = "detail-epic"
|
|
20
18
|
)
|
|
21
19
|
|
|
22
|
-
//
|
|
23
|
-
type
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
// ViewConfig holds terminal and overlay presentation state.
|
|
21
|
+
type ViewConfig struct {
|
|
22
|
+
Theme data.Theme
|
|
23
|
+
Overlay OverlayType
|
|
24
|
+
Width int
|
|
25
|
+
Height int
|
|
26
|
+
StatusMessage string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// DataState holds task, router, and filesystem state used by the board.
|
|
30
|
+
type DataState struct {
|
|
31
|
+
AllTasks []data.Task
|
|
32
|
+
Tasks map[data.ColumnType][]data.Task
|
|
33
|
+
Root string
|
|
34
|
+
EpicStatus map[string]string
|
|
35
|
+
RouterTask string
|
|
36
|
+
RouterState *data.RouterState
|
|
37
|
+
Watcher *fsnotify.Watcher
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// NavigationState holds board-column and detail scrolling state.
|
|
41
|
+
type NavigationState struct {
|
|
42
|
+
FocusedColumn data.ColumnType
|
|
43
|
+
FocusedTask int
|
|
44
|
+
ColumnOffsets map[data.ColumnType]int
|
|
45
|
+
DetailOffset int
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// EpicState holds epic list, sidebar, and detail overlay state.
|
|
49
|
+
type EpicState struct {
|
|
50
|
+
SelectedEpic string
|
|
51
|
+
Epics []string
|
|
52
|
+
EpicCursor int
|
|
53
|
+
EpicPanelFocus bool
|
|
54
|
+
EpicPanelCursor int
|
|
55
|
+
EpicDetailOffset int
|
|
56
|
+
EpicDetailEpic string
|
|
57
|
+
EpicDetailContent string
|
|
58
|
+
EpicDetailTab int // 0=Detail, 1=Audit
|
|
59
|
+
EpicAuditContent string // cached E##-Audit.md content
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ReleaseState holds release list and release picker state.
|
|
63
|
+
type ReleaseState struct {
|
|
29
64
|
SelectedRelease string
|
|
30
|
-
Epics []string
|
|
31
|
-
EpicCursor int
|
|
32
65
|
Releases []string
|
|
33
66
|
ReleaseEpics map[string][]string
|
|
34
67
|
ReleaseCursor int
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// DataAccessState holds board data-access implementations.
|
|
71
|
+
type DataAccessState struct {
|
|
72
|
+
Dependencies ModelDependencies
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Model holds all board state. Tasks are grouped by column for O(1) column access.
|
|
76
|
+
type Model struct {
|
|
77
|
+
ViewConfig
|
|
78
|
+
DataState
|
|
79
|
+
NavigationState
|
|
80
|
+
EpicState
|
|
81
|
+
ReleaseState
|
|
82
|
+
DataAccessState
|
|
42
83
|
}
|
|
43
84
|
|
|
44
85
|
// NewModel groups tasks by column and returns an initialized Model.
|
|
45
|
-
func NewModel(tasks []data.Task, release, epic string) Model {
|
|
86
|
+
func NewModel(tasks []data.Task, release, epic string, deps ...ModelDependencies) Model {
|
|
46
87
|
m := Model{
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
88
|
+
ViewConfig: ViewConfig{
|
|
89
|
+
Overlay: OverlayNone,
|
|
90
|
+
},
|
|
91
|
+
DataState: DataState{
|
|
92
|
+
AllTasks: append([]data.Task(nil), tasks...),
|
|
93
|
+
},
|
|
94
|
+
NavigationState: NavigationState{
|
|
95
|
+
FocusedColumn: data.ColumnPlanned,
|
|
96
|
+
FocusedTask: 0,
|
|
97
|
+
ColumnOffsets: newColumnOffsets(),
|
|
98
|
+
},
|
|
99
|
+
EpicState: EpicState{
|
|
100
|
+
SelectedEpic: epic,
|
|
101
|
+
},
|
|
102
|
+
ReleaseState: ReleaseState{
|
|
103
|
+
SelectedRelease: release,
|
|
104
|
+
},
|
|
105
|
+
DataAccessState: DataAccessState{
|
|
106
|
+
Dependencies: modelDependencies(deps),
|
|
107
|
+
},
|
|
53
108
|
}
|
|
54
109
|
m.refreshTasks()
|
|
55
110
|
return m
|
|
@@ -91,6 +146,15 @@ func (m *Model) refreshTasks() {
|
|
|
91
146
|
}
|
|
92
147
|
m.Tasks = groupedTasks(visible)
|
|
93
148
|
m.clampFocusedTask()
|
|
149
|
+
m.clampColumnOffsets()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
func newColumnOffsets() map[data.ColumnType]int {
|
|
153
|
+
return map[data.ColumnType]int{
|
|
154
|
+
data.ColumnPlanned: 0,
|
|
155
|
+
data.ColumnInProgress: 0,
|
|
156
|
+
data.ColumnDone: 0,
|
|
157
|
+
}
|
|
94
158
|
}
|
|
95
159
|
|
|
96
160
|
func (m *Model) refreshEpicsForRelease() {
|
|
@@ -103,18 +167,36 @@ func (m *Model) refreshEpicsForRelease() {
|
|
|
103
167
|
if len(m.Epics) == 0 {
|
|
104
168
|
m.SelectedEpic = ""
|
|
105
169
|
m.EpicCursor = 0
|
|
170
|
+
m.EpicPanelCursor = 0
|
|
171
|
+
m.EpicPanelFocus = false
|
|
106
172
|
return
|
|
107
173
|
}
|
|
108
174
|
|
|
109
175
|
for _, epic := range m.Epics {
|
|
110
176
|
if epic == m.SelectedEpic {
|
|
111
|
-
m.EpicCursor =
|
|
177
|
+
m.EpicCursor = sliceIndex(m.Epics, m.SelectedEpic)
|
|
178
|
+
m.clampEpicPanelCursor()
|
|
112
179
|
return
|
|
113
180
|
}
|
|
114
181
|
}
|
|
115
182
|
|
|
116
183
|
m.SelectedEpic = m.Epics[0]
|
|
117
184
|
m.EpicCursor = 0
|
|
185
|
+
m.clampEpicPanelCursor()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
func (m *Model) clampEpicPanelCursor() {
|
|
189
|
+
if len(m.Epics) == 0 {
|
|
190
|
+
m.EpicPanelCursor = 0
|
|
191
|
+
m.EpicPanelFocus = false
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
if m.EpicPanelCursor >= len(m.Epics) {
|
|
195
|
+
m.EpicPanelCursor = len(m.Epics) - 1
|
|
196
|
+
}
|
|
197
|
+
if m.EpicPanelCursor < 0 {
|
|
198
|
+
m.EpicPanelCursor = 0
|
|
199
|
+
}
|
|
118
200
|
}
|
|
119
201
|
|
|
120
202
|
func (m *Model) clampFocusedTask() {
|
|
@@ -131,27 +213,23 @@ func (m *Model) clampFocusedTask() {
|
|
|
131
213
|
}
|
|
132
214
|
}
|
|
133
215
|
|
|
134
|
-
func (m *Model)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
fi, err := os.Stat(routerPath)
|
|
138
|
-
if err != nil {
|
|
139
|
-
return err
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
content, err := os.ReadFile(routerPath)
|
|
143
|
-
if err != nil {
|
|
144
|
-
return err
|
|
216
|
+
func (m *Model) clampColumnOffsets() {
|
|
217
|
+
if m.ColumnOffsets == nil {
|
|
218
|
+
m.ColumnOffsets = newColumnOffsets()
|
|
145
219
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
220
|
+
for _, col := range columnOrder {
|
|
221
|
+
tasks := m.Tasks[col]
|
|
222
|
+
offset := m.ColumnOffsets[col]
|
|
223
|
+
if offset < 0 || len(tasks) == 0 {
|
|
224
|
+
m.ColumnOffsets[col] = 0
|
|
225
|
+
continue
|
|
226
|
+
}
|
|
227
|
+
if offset >= len(tasks) {
|
|
228
|
+
m.ColumnOffsets[col] = len(tasks) - 1
|
|
229
|
+
}
|
|
151
230
|
}
|
|
231
|
+
}
|
|
152
232
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return data.WriteRouterState(m.Root, state, fi.ModTime())
|
|
233
|
+
func taskDone(task data.Task) bool {
|
|
234
|
+
return task.Column == data.ColumnDone
|
|
157
235
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"strings"
|
|
8
|
+
|
|
9
|
+
"github.com/opencode/savepoint/internal/data"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
const plainNonTTYWarning = "[non-interactive mode — run in a TTY to launch the board UI]"
|
|
13
|
+
const plainAuditSignal = "[◆ audit proposals pending]"
|
|
14
|
+
|
|
15
|
+
// RenderPlainTable renders a plain text three-column task table for non-TTY output.
|
|
16
|
+
func RenderPlainTable(model Model) string {
|
|
17
|
+
var b strings.Builder
|
|
18
|
+
|
|
19
|
+
fmt.Fprintln(&b, plainNonTTYWarning)
|
|
20
|
+
if hasAuditProposals(model.Root) {
|
|
21
|
+
fmt.Fprintln(&b, plainAuditSignal)
|
|
22
|
+
}
|
|
23
|
+
fmt.Fprintln(&b)
|
|
24
|
+
|
|
25
|
+
cols := []struct {
|
|
26
|
+
label string
|
|
27
|
+
col data.ColumnType
|
|
28
|
+
}{
|
|
29
|
+
{"PLANNED", data.ColumnPlanned},
|
|
30
|
+
{"IN PROGRESS", data.ColumnInProgress},
|
|
31
|
+
{"DONE", data.ColumnDone},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for _, c := range cols {
|
|
35
|
+
tasks := model.Tasks[c.col]
|
|
36
|
+
fmt.Fprintln(&b, c.label)
|
|
37
|
+
if len(tasks) == 0 {
|
|
38
|
+
fmt.Fprintln(&b, " (none)")
|
|
39
|
+
}
|
|
40
|
+
for _, t := range tasks {
|
|
41
|
+
title := t.Title
|
|
42
|
+
if title == "" {
|
|
43
|
+
title = "(no title)"
|
|
44
|
+
}
|
|
45
|
+
fmt.Fprintf(&b, " %-52s %s\n", t.ID, title)
|
|
46
|
+
}
|
|
47
|
+
fmt.Fprintln(&b)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return b.String()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// hasAuditProposals reports whether any audit file under root contains a Proposed Changes section.
|
|
54
|
+
func hasAuditProposals(root string) bool {
|
|
55
|
+
releasesDir := filepath.Join(root, "releases")
|
|
56
|
+
releases, err := os.ReadDir(releasesDir)
|
|
57
|
+
if err != nil {
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
for _, r := range releases {
|
|
61
|
+
if !r.IsDir() {
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
epicsDir := filepath.Join(releasesDir, r.Name(), "epics")
|
|
65
|
+
epics, err := os.ReadDir(epicsDir)
|
|
66
|
+
if err != nil {
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
for _, e := range epics {
|
|
70
|
+
if !e.IsDir() {
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
short := e.Name()
|
|
74
|
+
if idx := strings.Index(short, "-"); idx >= 0 {
|
|
75
|
+
short = short[:idx]
|
|
76
|
+
}
|
|
77
|
+
auditPath := filepath.Join(epicsDir, e.Name(), short+"-Audit.md")
|
|
78
|
+
raw, err := os.ReadFile(auditPath)
|
|
79
|
+
if err != nil {
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
if strings.Contains(string(raw), "## Proposed Changes") {
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
|
|
9
|
+
"github.com/opencode/savepoint/internal/data"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
func TestRenderPlainTable_warningBanner(t *testing.T) {
|
|
13
|
+
m := NewModel(nil, "", "")
|
|
14
|
+
got := RenderPlainTable(m)
|
|
15
|
+
if !strings.Contains(got, plainNonTTYWarning) {
|
|
16
|
+
t.Errorf("RenderPlainTable missing warning banner, got:\n%s", got)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func TestRenderPlainTable_columnHeaders(t *testing.T) {
|
|
21
|
+
m := NewModel(nil, "", "")
|
|
22
|
+
got := RenderPlainTable(m)
|
|
23
|
+
for _, header := range []string{"PLANNED", "IN PROGRESS", "DONE"} {
|
|
24
|
+
if !strings.Contains(got, header) {
|
|
25
|
+
t.Errorf("RenderPlainTable missing column header %q", header)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func TestRenderPlainTable_taskIDsAndTitles(t *testing.T) {
|
|
31
|
+
tasks := []data.Task{
|
|
32
|
+
{ID: "E08/T001", Title: "CLI entrypoint", Column: data.ColumnDone},
|
|
33
|
+
{ID: "E08/T002", Title: "Non-TTY fallback", Column: data.ColumnInProgress},
|
|
34
|
+
{ID: "E08/T003", Title: "TUI app shell", Column: data.ColumnPlanned},
|
|
35
|
+
}
|
|
36
|
+
m := NewModel(tasks, "", "")
|
|
37
|
+
got := RenderPlainTable(m)
|
|
38
|
+
|
|
39
|
+
for _, want := range []string{"E08/T001", "CLI entrypoint", "E08/T002", "Non-TTY fallback", "E08/T003", "TUI app shell"} {
|
|
40
|
+
if !strings.Contains(got, want) {
|
|
41
|
+
t.Errorf("RenderPlainTable missing %q", want)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func TestRenderPlainTable_noneWhenColumnEmpty(t *testing.T) {
|
|
47
|
+
m := NewModel(nil, "", "")
|
|
48
|
+
got := RenderPlainTable(m)
|
|
49
|
+
if !strings.Contains(got, "(none)") {
|
|
50
|
+
t.Errorf("RenderPlainTable should show (none) for empty columns")
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func TestRenderPlainTable_auditSignalWhenProposalsExist(t *testing.T) {
|
|
55
|
+
root := t.TempDir()
|
|
56
|
+
epicDir := filepath.Join(root, "releases", "v1", "epics", "E01-test")
|
|
57
|
+
if err := os.MkdirAll(epicDir, 0755); err != nil {
|
|
58
|
+
t.Fatal(err)
|
|
59
|
+
}
|
|
60
|
+
auditContent := "---\ntype: audit\n---\n## Main Findings\nOK\n\n## Proposed Changes\n\n### Target File\nfoo.go\n"
|
|
61
|
+
if err := os.WriteFile(filepath.Join(epicDir, "E01-Audit.md"), []byte(auditContent), 0644); err != nil {
|
|
62
|
+
t.Fatal(err)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
m := NewModel(nil, "", "")
|
|
66
|
+
m.Root = root
|
|
67
|
+
got := RenderPlainTable(m)
|
|
68
|
+
if !strings.Contains(got, plainAuditSignal) {
|
|
69
|
+
t.Errorf("RenderPlainTable missing audit signal when proposals exist, got:\n%s", got)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
func TestRenderPlainTable_noAuditSignalWhenNone(t *testing.T) {
|
|
74
|
+
root := t.TempDir()
|
|
75
|
+
m := NewModel(nil, "", "")
|
|
76
|
+
m.Root = root
|
|
77
|
+
got := RenderPlainTable(m)
|
|
78
|
+
if strings.Contains(got, plainAuditSignal) {
|
|
79
|
+
t.Errorf("RenderPlainTable should not show audit signal when no proposals exist")
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func TestHasAuditProposals_detectsSection(t *testing.T) {
|
|
84
|
+
root := t.TempDir()
|
|
85
|
+
epicDir := filepath.Join(root, "releases", "v1", "epics", "E02-slug")
|
|
86
|
+
if err := os.MkdirAll(epicDir, 0755); err != nil {
|
|
87
|
+
t.Fatal(err)
|
|
88
|
+
}
|
|
89
|
+
content := "## Main Findings\nAll good.\n\n## Proposed Changes\n\n### Target File\nbar.go\n"
|
|
90
|
+
if err := os.WriteFile(filepath.Join(epicDir, "E02-Audit.md"), []byte(content), 0644); err != nil {
|
|
91
|
+
t.Fatal(err)
|
|
92
|
+
}
|
|
93
|
+
if !hasAuditProposals(root) {
|
|
94
|
+
t.Error("hasAuditProposals should return true when Proposed Changes section exists")
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
func TestHasAuditProposals_noProposals(t *testing.T) {
|
|
99
|
+
root := t.TempDir()
|
|
100
|
+
epicDir := filepath.Join(root, "releases", "v1", "epics", "E03-slug")
|
|
101
|
+
if err := os.MkdirAll(epicDir, 0755); err != nil {
|
|
102
|
+
t.Fatal(err)
|
|
103
|
+
}
|
|
104
|
+
content := "## Main Findings\nAll good.\n"
|
|
105
|
+
if err := os.WriteFile(filepath.Join(epicDir, "E03-Audit.md"), []byte(content), 0644); err != nil {
|
|
106
|
+
t.Fatal(err)
|
|
107
|
+
}
|
|
108
|
+
if hasAuditProposals(root) {
|
|
109
|
+
t.Error("hasAuditProposals should return false when no Proposed Changes section")
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
func TestHasAuditProposals_missingRoot(t *testing.T) {
|
|
114
|
+
if hasAuditProposals("/nonexistent/path/xyz") {
|
|
115
|
+
t.Error("hasAuditProposals should return false for missing root")
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -31,12 +31,4 @@ func RenderReleaseDropdown(releases []string, cursor int, width int) string {
|
|
|
31
31
|
return styles.EpicPanel.Width(width).Render(strings.Join(lines, "\n"))
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
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
|
-
}
|
|
34
|
+
|
|
@@ -40,17 +40,17 @@ func TestRenderReleaseDropdown_hintsPresent(t *testing.T) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
func
|
|
43
|
+
func TestSliceIndexRelease_found(t *testing.T) {
|
|
44
44
|
releases := []string{"v1", "v2", "v3"}
|
|
45
|
-
if got :=
|
|
46
|
-
t.Errorf("
|
|
45
|
+
if got := sliceIndex(releases, "v2"); got != 1 {
|
|
46
|
+
t.Errorf("sliceIndex = %d, want 1", got)
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
func
|
|
50
|
+
func TestSliceIndexRelease_notFound(t *testing.T) {
|
|
51
51
|
releases := []string{"v1", "v2"}
|
|
52
|
-
if got :=
|
|
53
|
-
t.Errorf("
|
|
52
|
+
if got := sliceIndex(releases, "v9"); got != 0 {
|
|
53
|
+
t.Errorf("sliceIndex = %d, want 0", got)
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/charmbracelet/lipgloss"
|
|
8
|
+
"github.com/muesli/termenv"
|
|
9
|
+
"github.com/opencode/savepoint/internal/data"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
func TestRenderPolicy_noBackgroundEscapes(t *testing.T) {
|
|
13
|
+
lipgloss.SetColorProfile(termenv.ANSI256)
|
|
14
|
+
|
|
15
|
+
task := data.Task{
|
|
16
|
+
ID: "E05-tasking-permissions/T004-implement-m-hotkey",
|
|
17
|
+
Title: "Implement router priority",
|
|
18
|
+
Release: "v1.1",
|
|
19
|
+
Epic: "E05-tasking-permissions",
|
|
20
|
+
Column: data.ColumnInProgress,
|
|
21
|
+
Stage: data.StageBuild,
|
|
22
|
+
}
|
|
23
|
+
m := NewModel([]data.Task{
|
|
24
|
+
{
|
|
25
|
+
ID: task.ID,
|
|
26
|
+
Title: task.Title,
|
|
27
|
+
Release: task.Release,
|
|
28
|
+
Epic: task.Epic,
|
|
29
|
+
Column: task.Column,
|
|
30
|
+
Stage: task.Stage,
|
|
31
|
+
},
|
|
32
|
+
}, "v1.1", "E05-tasking-permissions")
|
|
33
|
+
m.Width = 120
|
|
34
|
+
m.Height = 30
|
|
35
|
+
m.FocusedColumn = data.ColumnInProgress
|
|
36
|
+
m.RouterState = &data.RouterState{
|
|
37
|
+
State: "task-building",
|
|
38
|
+
Release: "v1.1",
|
|
39
|
+
Epic: "E05-tasking-permissions",
|
|
40
|
+
Task: "E05-tasking-permissions/T004-implement-m-hotkey",
|
|
41
|
+
NextAction: "Build E05-tasking-permissions/T004-implement-m-hotkey.",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
cases := map[string]string{
|
|
45
|
+
"board": m.View(),
|
|
46
|
+
"card": RenderCard(task, 30, true, m.RouterState),
|
|
47
|
+
"detail": RenderDetail(task, 60, m.RouterState, 0, 0),
|
|
48
|
+
"epic dropdown": RenderEpicDropdown([]string{"E05-tasking-permissions"}, 0, 40),
|
|
49
|
+
"release dropdown": RenderReleaseDropdown([]string{"v1.1"}, 0, 40),
|
|
50
|
+
"help": RenderHelp(60),
|
|
51
|
+
}
|
|
52
|
+
for name, got := range cases {
|
|
53
|
+
assertNoBackgroundEscapes(t, name, got)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func assertNoBackgroundEscapes(t *testing.T, name, got string) {
|
|
58
|
+
t.Helper()
|
|
59
|
+
for _, escape := range []string{"\x1b[48;", "\x1b[40m"} {
|
|
60
|
+
if strings.Contains(got, escape) {
|
|
61
|
+
t.Fatalf("%s emitted background escape prefix %q in %q", name, escape, got)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func TestRenderPolicy_usesSingleLineBorders(t *testing.T) {
|
|
67
|
+
m := NewModel([]data.Task{{ID: "T001", Title: "Task", Column: data.ColumnPlanned}}, "v1", "E01")
|
|
68
|
+
m.Width = 120
|
|
69
|
+
got := m.View()
|
|
70
|
+
|
|
71
|
+
if strings.Contains(got, "╭") || strings.Contains(got, "╮") || strings.Contains(got, "╰") || strings.Contains(got, "╯") {
|
|
72
|
+
t.Fatalf("View should use single-line borders, got rounded border glyphs in %q", got)
|
|
73
|
+
}
|
|
74
|
+
if !strings.Contains(got, "┌") || !strings.Contains(got, "┐") || !strings.Contains(got, "└") || !strings.Contains(got, "┘") {
|
|
75
|
+
t.Fatalf("View missing expected single-line border glyphs in %q", got)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"github.com/opencode/savepoint/internal/data"
|
|
5
|
+
"github.com/opencode/savepoint/internal/styles"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
const statusGlyphDefault = " "
|
|
9
|
+
|
|
10
|
+
func statusGlyph(status string) string {
|
|
11
|
+
switch status {
|
|
12
|
+
case string(data.ColumnPlanned):
|
|
13
|
+
return styles.CardMeta.Render("○")
|
|
14
|
+
case string(data.ColumnInProgress):
|
|
15
|
+
return styles.GlyphBuild.Render("▶")
|
|
16
|
+
case string(data.ColumnDone):
|
|
17
|
+
return styles.TagDone.Render("◉")
|
|
18
|
+
case "audited":
|
|
19
|
+
return styles.TagDone.Render("✓")
|
|
20
|
+
default:
|
|
21
|
+
return statusGlyphDefault
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
|
|
6
|
+
"github.com/charmbracelet/lipgloss"
|
|
7
|
+
"github.com/muesli/termenv"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// applyColorProfile detects terminal color support and configures lipgloss.
|
|
11
|
+
// Priority: NO_COLOR → COLORTERM env hint → termenv auto-detect.
|
|
12
|
+
func applyColorProfile() {
|
|
13
|
+
lipgloss.SetColorProfile(detectProfile())
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
func detectProfile() termenv.Profile {
|
|
17
|
+
if os.Getenv("NO_COLOR") != "" {
|
|
18
|
+
return termenv.Ascii
|
|
19
|
+
}
|
|
20
|
+
if c := os.Getenv("COLORTERM"); c == "truecolor" || c == "24bit" {
|
|
21
|
+
return termenv.TrueColor
|
|
22
|
+
}
|
|
23
|
+
return termenv.ColorProfile()
|
|
24
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package board
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"testing"
|
|
5
|
+
|
|
6
|
+
"github.com/muesli/termenv"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
func TestDetectProfileNOCOLOR(t *testing.T) {
|
|
10
|
+
t.Setenv("NO_COLOR", "1")
|
|
11
|
+
t.Setenv("COLORTERM", "")
|
|
12
|
+
if got := detectProfile(); got != termenv.Ascii {
|
|
13
|
+
t.Fatalf("want Ascii, got %v", got)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func TestDetectProfileTruecolor(t *testing.T) {
|
|
18
|
+
t.Setenv("NO_COLOR", "")
|
|
19
|
+
t.Setenv("COLORTERM", "truecolor")
|
|
20
|
+
if got := detectProfile(); got != termenv.TrueColor {
|
|
21
|
+
t.Fatalf("want TrueColor, got %v", got)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func TestDetectProfile24bit(t *testing.T) {
|
|
26
|
+
t.Setenv("NO_COLOR", "")
|
|
27
|
+
t.Setenv("COLORTERM", "24bit")
|
|
28
|
+
if got := detectProfile(); got != termenv.TrueColor {
|
|
29
|
+
t.Fatalf("want TrueColor, got %v", got)
|
|
30
|
+
}
|
|
31
|
+
}
|