savepoint 1.0.2 → 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 +12 -1
- package/.golangci.yml +11 -0
- package/.savepoint/Design.md +37 -36
- package/.savepoint/{audit/v1.1/E02-cross-platform-compatibility/proposals.md → releases/v1.1/epics/E02-cross-platform-compatibility/E02-Audit.md} +48 -38
- 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 +14 -1
- package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T006-forced-256-color-profile.md +3 -3
- package/.savepoint/{audit/v1.1/E04-epic-navigation/proposals.md → releases/v1.1/epics/E04-epic-navigation/E04-Audit.md} +65 -54
- 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 +25 -16
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T001-update-agents-md.md +17 -6
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T002-update-router-md.md +15 -5
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T003-update-design-md.md +19 -5
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T004-implement-m-hotkey.md +11 -1
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T005-update-help-overlay.md +9 -6
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T006-tests-and-quality-gates.md +29 -13
- 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 +67 -7
- package/.savepoint/router.md +9 -16
- package/AGENTS.md +38 -23
- package/README.md +0 -1
- package/agent-skills/savepoint-audit/SKILL.md +86 -34
- package/agent-skills/savepoint-build-task/SKILL.md +7 -2
- package/agent-skills/savepoint-create-plan/SKILL.md +7 -2
- package/agent-skills/savepoint-create-task/SKILL.md +44 -31
- package/agent-skills/savepoint-draft-prd/SKILL.md +7 -2
- package/agent-skills/savepoint-system-design/SKILL.md +7 -2
- 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 +40 -36
- package/internal/board/board_test.go +27 -82
- package/internal/board/card.go +43 -23
- package/internal/board/card_test.go +41 -5
- package/internal/board/column.go +44 -13
- package/internal/board/column_test.go +5 -2
- package/internal/board/detail.go +0 -47
- package/internal/board/epic_panel.go +118 -22
- package/internal/board/epic_panel_test.go +302 -17
- 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/model.go +79 -118
- 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/status.go +4 -4
- 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 +325 -215
- package/internal/board/update_test.go +299 -18
- package/internal/board/util.go +76 -0
- package/internal/board/view.go +31 -28
- package/internal/board/view_test.go +12 -2
- package/internal/board/watch.go +35 -5
- package/internal/buildtool/main.go +2 -10
- 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 +21 -6
- package/internal/data/parser_test.go +31 -7
- package/internal/data/task.go +0 -9
- 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/styles_test.go +133 -0
- package/internal/testutil/fixture.go +113 -0
- package/internal/testutil/fs.go +26 -0
- package/main.go +101 -4
- 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 +10 -10
- package/templates/project/AGENTS.md +33 -21
- 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 +33 -28
- package/templates/prompts/design.prompt.md +3 -1
- package/.savepoint/audit/v1/E01/proposals.md +0 -168
- package/.savepoint/audit/v1/E01/snapshot.md +0 -78
- package/.savepoint/audit/v1/E01-go-setup/proposals.md +0 -166
- package/.savepoint/audit/v1/E01-go-setup/snapshot.md +0 -71
- package/.savepoint/audit/v1/E01-scaffolding/proposals/AGENTS.md +0 -66
- package/.savepoint/audit/v1/E01-scaffolding/proposals/Design.md +0 -210
- package/.savepoint/audit/v1/E01-scaffolding/proposals/epic-Design.md +0 -117
- package/.savepoint/audit/v1/E01-scaffolding/proposals/quality-review.md +0 -101
- package/.savepoint/audit/v1/E01-scaffolding/snapshot.md +0 -54
- package/.savepoint/audit/v1/E02-data-model/snapshot.md +0 -128
- package/.savepoint/audit/v1/E02-data-readers/proposals.md +0 -123
- package/.savepoint/audit/v1/E02-data-readers/snapshot.md +0 -54
- package/.savepoint/audit/v1/E03-board-tui-core/proposals.md +0 -146
- package/.savepoint/audit/v1/E03-board-tui-core/snapshot.md +0 -57
- package/.savepoint/audit/v1/E03-cli-foundation/snapshot.md +0 -106
- package/.savepoint/audit/v1/E04-board-components/proposals.md +0 -118
- package/.savepoint/audit/v1/E04-board-components/snapshot.md +0 -77
- package/.savepoint/audit/v1/E04-templates-and-prompts/snapshot.md +0 -115
- package/.savepoint/audit/v1/E05-init-command/snapshot.md +0 -125
- package/.savepoint/audit/v1/E05-phase-transitions/proposals.md +0 -83
- package/.savepoint/audit/v1/E05-phase-transitions/snapshot.md +0 -36
- package/.savepoint/audit/v1/E06-atari-noir-layout/proposals.md +0 -130
- package/.savepoint/audit/v1/E06-atari-noir-layout/snapshot.md +0 -84
- package/.savepoint/audit/v1/E06-tui-board/snapshot.md +0 -64
- package/.savepoint/audit/v1/E07-audit-pipeline/snapshot.md +0 -165
- package/.savepoint/audit/v1/E08-board-workflow-cleanup/snapshot.md +0 -65
- package/.savepoint/audit/v1.1/E02-cross-platform-compatibility/snapshot.md +0 -41
- package/.savepoint/audit/v1.1/E04-epic-navigation/snapshot.md +0 -48
- package/ink-cli-ui-design.zip +0 -0
- package/savepoint.exe +0 -0
package/internal/board/card.go
CHANGED
|
@@ -4,6 +4,7 @@ import (
|
|
|
4
4
|
"fmt"
|
|
5
5
|
"strings"
|
|
6
6
|
|
|
7
|
+
"github.com/charmbracelet/lipgloss"
|
|
7
8
|
"github.com/opencode/savepoint/internal/data"
|
|
8
9
|
"github.com/opencode/savepoint/internal/styles"
|
|
9
10
|
)
|
|
@@ -24,18 +25,20 @@ func RenderCard(t data.Task, width int, focused bool, routerState *data.RouterSt
|
|
|
24
25
|
inner = 2
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
glyph := taskGlyph(t, routerState)
|
|
29
|
+
phase := taskPhaseText(t)
|
|
30
|
+
idWidth := inner - 2
|
|
31
|
+
if phase != "" {
|
|
32
|
+
idWidth -= lipgloss.Width(phase) + 1
|
|
33
|
+
}
|
|
34
|
+
if idWidth < 1 {
|
|
35
|
+
idWidth = 1
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
idLine := fmt.Sprintf("%s %s", glyph, truncate(shortID(t.ID), idWidth))
|
|
39
|
+
if phase != "" && lipgloss.Width(idLine)+1+lipgloss.Width(phase) <= inner {
|
|
40
|
+
idLine += " " + phase
|
|
41
|
+
}
|
|
39
42
|
titleLine := styles.CardMeta.Render(strings.Join(WrapText(t.Title, inner), "\n"))
|
|
40
43
|
|
|
41
44
|
content := idLine + "\n" + titleLine
|
|
@@ -46,6 +49,33 @@ func RenderCard(t data.Task, width int, focused bool, routerState *data.RouterSt
|
|
|
46
49
|
return styles.Card.Width(width).Render(content)
|
|
47
50
|
}
|
|
48
51
|
|
|
52
|
+
func taskGlyph(t data.Task, routerState *data.RouterState) string {
|
|
53
|
+
if t.Column == data.ColumnInProgress {
|
|
54
|
+
return phaseGlyphStyled(t.Stage)
|
|
55
|
+
}
|
|
56
|
+
if t.Column == data.ColumnDone {
|
|
57
|
+
return styles.GlyphBuild.Render(glyphBuild)
|
|
58
|
+
}
|
|
59
|
+
if isRouterPriority(t, routerState) {
|
|
60
|
+
return styles.TagDone.Render(glyphBuild)
|
|
61
|
+
}
|
|
62
|
+
if t.Status != "" {
|
|
63
|
+
return statusGlyph(t.Status)
|
|
64
|
+
}
|
|
65
|
+
return phaseGlyphStyled(t.Stage)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func taskPhaseText(t data.Task) string {
|
|
69
|
+
switch t.Column {
|
|
70
|
+
case data.ColumnInProgress:
|
|
71
|
+
return styles.CardMeta.Render(strings.ToUpper(phaseLabel(t.Stage)))
|
|
72
|
+
case data.ColumnDone:
|
|
73
|
+
return styles.CardMeta.Render("DONE")
|
|
74
|
+
default:
|
|
75
|
+
return ""
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
49
79
|
func phaseGlyphStyled(stage data.ProgressStage) string {
|
|
50
80
|
switch stage {
|
|
51
81
|
case data.StageTest:
|
|
@@ -96,14 +126,4 @@ func shortID(id string) string {
|
|
|
96
126
|
return id
|
|
97
127
|
}
|
|
98
128
|
|
|
99
|
-
|
|
100
|
-
func truncate(s string, max int) string {
|
|
101
|
-
runes := []rune(s)
|
|
102
|
-
if len(runes) <= max {
|
|
103
|
-
return s
|
|
104
|
-
}
|
|
105
|
-
if max <= 1 {
|
|
106
|
-
return "…"
|
|
107
|
-
}
|
|
108
|
-
return string(runes[:max-1]) + "…"
|
|
109
|
-
}
|
|
129
|
+
|
|
@@ -194,13 +194,12 @@ func TestRenderCard_doneTaskUsesOrangeBuildGlyph(t *testing.T) {
|
|
|
194
194
|
func TestRenderCard_explicitStatusUsesUnifiedGlyph(t *testing.T) {
|
|
195
195
|
tests := []struct {
|
|
196
196
|
name string
|
|
197
|
-
status
|
|
197
|
+
status string
|
|
198
198
|
glyph string
|
|
199
199
|
}{
|
|
200
|
-
{"planned", data.
|
|
201
|
-
{"
|
|
202
|
-
{"
|
|
203
|
-
{"audited", data.StatusAudited, "✓"},
|
|
200
|
+
{"planned", string(data.ColumnPlanned), "○"},
|
|
201
|
+
{"done", string(data.ColumnDone), "◉"},
|
|
202
|
+
{"audited", "audited", "✓"},
|
|
204
203
|
}
|
|
205
204
|
|
|
206
205
|
for _, tt := range tests {
|
|
@@ -216,3 +215,40 @@ func TestRenderCard_explicitStatusUsesUnifiedGlyph(t *testing.T) {
|
|
|
216
215
|
})
|
|
217
216
|
}
|
|
218
217
|
}
|
|
218
|
+
|
|
219
|
+
func TestRenderCard_inProgressShowsPhaseText(t *testing.T) {
|
|
220
|
+
tests := []struct {
|
|
221
|
+
name string
|
|
222
|
+
stage data.ProgressStage
|
|
223
|
+
label string
|
|
224
|
+
glyph string
|
|
225
|
+
}{
|
|
226
|
+
{"build", data.StageBuild, "BUILD", glyphBuild},
|
|
227
|
+
{"test", data.StageTest, "TEST", glyphTest},
|
|
228
|
+
{"audit", data.StageAudit, "AUDIT", glyphAudit},
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for _, tt := range tests {
|
|
232
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
233
|
+
task := data.Task{ID: "T1", Column: data.ColumnInProgress, Status: string(data.ColumnInProgress), Stage: tt.stage}
|
|
234
|
+
got := RenderCard(task, 30, false, nil)
|
|
235
|
+
if !strings.Contains(got, tt.label) {
|
|
236
|
+
t.Errorf("RenderCard missing phase label %q", tt.label)
|
|
237
|
+
}
|
|
238
|
+
if !strings.Contains(got, tt.glyph) {
|
|
239
|
+
t.Errorf("RenderCard missing phase glyph %q", tt.glyph)
|
|
240
|
+
}
|
|
241
|
+
if strings.Contains(got, "▶") {
|
|
242
|
+
t.Error("RenderCard should not use generic in_progress glyph when phase is available")
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
func TestRenderCard_doneShowsDoneText(t *testing.T) {
|
|
249
|
+
task := data.Task{ID: "T1", Column: data.ColumnDone, Status: string(data.ColumnDone)}
|
|
250
|
+
got := RenderCard(task, 30, false, nil)
|
|
251
|
+
if !strings.Contains(got, "DONE") {
|
|
252
|
+
t.Error("RenderCard missing DONE phase label")
|
|
253
|
+
}
|
|
254
|
+
}
|
package/internal/board/column.go
CHANGED
|
@@ -28,17 +28,54 @@ func RenderColumn(tasks []data.Task, col data.ColumnType, width, maxHeight, offs
|
|
|
28
28
|
if len(tasks) == 0 {
|
|
29
29
|
lines = append(lines, styles.TaskItem.Render("(empty)"))
|
|
30
30
|
} else {
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
contentBudget := maxHeight - 2
|
|
32
|
+
if contentBudget < 1 {
|
|
33
|
+
contentBudget = 1
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
reserveAbove := 0
|
|
37
|
+
if offset > 0 {
|
|
38
|
+
reserveAbove = 1
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type cardEntry struct {
|
|
42
|
+
card string
|
|
43
|
+
lines int
|
|
44
|
+
}
|
|
45
|
+
cardEntries := make([]cardEntry, 0, len(tasks)-offset)
|
|
46
|
+
for i := offset; i < len(tasks); i++ {
|
|
47
|
+
c := RenderCard(tasks[i], inner, focused && i == focusedTask, routerState)
|
|
48
|
+
cardEntries = append(cardEntries, cardEntry{card: c, lines: strings.Count(c, "\n") + 1})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
usedLines := reserveAbove
|
|
52
|
+
endIdx := 0
|
|
53
|
+
for endIdx < len(cardEntries) {
|
|
54
|
+
needsMore := endIdx < len(cardEntries)-1
|
|
55
|
+
bottomReserve := 0
|
|
56
|
+
if needsMore {
|
|
57
|
+
bottomReserve = 1
|
|
58
|
+
}
|
|
59
|
+
if usedLines+cardEntries[endIdx].lines+bottomReserve > contentBudget {
|
|
60
|
+
break
|
|
61
|
+
}
|
|
62
|
+
usedLines += cardEntries[endIdx].lines
|
|
63
|
+
endIdx++
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if endIdx == 0 && len(cardEntries) > 0 {
|
|
67
|
+
endIdx = 1
|
|
68
|
+
}
|
|
69
|
+
|
|
33
70
|
if offset > 0 {
|
|
34
71
|
lines = append(lines, renderScrollIndicator("↑", offset, "above"))
|
|
35
72
|
}
|
|
36
|
-
for i
|
|
37
|
-
|
|
38
|
-
lines = append(lines, RenderCard(t, inner, focused && taskIndex == focusedTask, routerState))
|
|
73
|
+
for i := 0; i < endIdx; i++ {
|
|
74
|
+
lines = append(lines, cardEntries[i].card)
|
|
39
75
|
}
|
|
40
|
-
if
|
|
41
|
-
|
|
76
|
+
if endIdx < len(cardEntries) {
|
|
77
|
+
remaining := len(tasks) - (offset + endIdx)
|
|
78
|
+
lines = append(lines, renderScrollIndicator("↓", remaining, "more"))
|
|
42
79
|
}
|
|
43
80
|
}
|
|
44
81
|
|
|
@@ -88,9 +125,3 @@ func columnTitle(col data.ColumnType) string {
|
|
|
88
125
|
}
|
|
89
126
|
}
|
|
90
127
|
|
|
91
|
-
func taskLabel(t data.Task) string {
|
|
92
|
-
if t.Title == "" {
|
|
93
|
-
return t.ID
|
|
94
|
-
}
|
|
95
|
-
return fmt.Sprintf("%s %s", t.ID, t.Title)
|
|
96
|
-
}
|
|
@@ -118,12 +118,15 @@ func TestRenderColumn_viewportShowsScrollIndicators(t *testing.T) {
|
|
|
118
118
|
if !strings.Contains(got, "↑ 1 above") {
|
|
119
119
|
t.Error("RenderColumn missing above indicator")
|
|
120
120
|
}
|
|
121
|
-
if !strings.Contains(got, "↓
|
|
122
|
-
t.
|
|
121
|
+
if !strings.Contains(got, "↓ 2 more") {
|
|
122
|
+
t.Errorf("RenderColumn missing more indicator, got:\n%s", got)
|
|
123
123
|
}
|
|
124
124
|
if strings.Contains(got, "Task one") {
|
|
125
125
|
t.Error("RenderColumn should not render tasks above viewport")
|
|
126
126
|
}
|
|
127
|
+
if strings.Contains(got, "Task three") {
|
|
128
|
+
t.Error("RenderColumn should not render tasks that don't fit budget")
|
|
129
|
+
}
|
|
127
130
|
if strings.Contains(got, "Task four") {
|
|
128
131
|
t.Error("RenderColumn should not render tasks below viewport")
|
|
129
132
|
}
|
package/internal/board/detail.go
CHANGED
|
@@ -182,51 +182,4 @@ func phaseLabel(s data.ProgressStage) string {
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
func WrapText(s string, width int) []string {
|
|
186
|
-
if width < 4 {
|
|
187
|
-
width = 4
|
|
188
|
-
}
|
|
189
|
-
words := strings.Fields(s)
|
|
190
|
-
if len(words) == 0 {
|
|
191
|
-
return nil
|
|
192
|
-
}
|
|
193
|
-
lines := []string{}
|
|
194
|
-
current := ""
|
|
195
|
-
for _, word := range words {
|
|
196
|
-
if len([]rune(word)) > width {
|
|
197
|
-
if current != "" {
|
|
198
|
-
lines = append(lines, current)
|
|
199
|
-
current = ""
|
|
200
|
-
}
|
|
201
|
-
lines = append(lines, SplitLongWord(word, width)...)
|
|
202
|
-
continue
|
|
203
|
-
}
|
|
204
|
-
if current == "" {
|
|
205
|
-
current = word
|
|
206
|
-
continue
|
|
207
|
-
}
|
|
208
|
-
if len([]rune(current))+1+len([]rune(word)) <= width {
|
|
209
|
-
current += " " + word
|
|
210
|
-
continue
|
|
211
|
-
}
|
|
212
|
-
lines = append(lines, current)
|
|
213
|
-
current = word
|
|
214
|
-
}
|
|
215
|
-
if current != "" {
|
|
216
|
-
lines = append(lines, current)
|
|
217
|
-
}
|
|
218
|
-
return lines
|
|
219
|
-
}
|
|
220
185
|
|
|
221
|
-
func SplitLongWord(word string, width int) []string {
|
|
222
|
-
runes := []rune(word)
|
|
223
|
-
lines := []string{}
|
|
224
|
-
for len(runes) > width {
|
|
225
|
-
lines = append(lines, string(runes[:width]))
|
|
226
|
-
runes = runes[width:]
|
|
227
|
-
}
|
|
228
|
-
if len(runes) > 0 {
|
|
229
|
-
lines = append(lines, string(runes))
|
|
230
|
-
}
|
|
231
|
-
return lines
|
|
232
|
-
}
|
|
@@ -8,33 +8,40 @@ import (
|
|
|
8
8
|
)
|
|
9
9
|
|
|
10
10
|
// RenderEpicDetail renders an overlay showing the content of an E##-Detail.md file.
|
|
11
|
-
func RenderEpicDetail(epicSlug, content string, overlayW, maxHeight, offset int) string {
|
|
11
|
+
func RenderEpicDetail(epicSlug, content string, overlayW, maxHeight, offset int, tab int) string {
|
|
12
12
|
inner := overlayW - detailBorderPad
|
|
13
13
|
if inner < 4 {
|
|
14
14
|
inner = 4
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
tabIndicator := renderTabIndicator(tab, inner)
|
|
17
18
|
lines := []string{
|
|
18
19
|
styles.EpicTitleFocused.Render("EPIC DETAIL"),
|
|
19
|
-
|
|
20
|
+
tabIndicator,
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
body := epicDetailBody(content, inner)
|
|
23
|
-
body = append(body, "", styles.CardMeta.Render("esc:close"))
|
|
24
|
-
lines = append(lines, visibleDetailLines(body, maxHeight-detailVerticalOverhead, offset)...)
|
|
24
|
+
body = append(body, "", styles.CardMeta.Render("1:Detail 2:Audit esc:close"))
|
|
25
|
+
lines = append(lines, visibleDetailLines(body, maxHeight-detailVerticalOverhead-1, offset)...)
|
|
25
26
|
|
|
26
27
|
return styles.EpicDetailOverlay.Width(overlayW).Render(strings.Join(lines, "\n"))
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if
|
|
32
|
-
|
|
30
|
+
func renderTabIndicator(tab int, width int) string {
|
|
31
|
+
var detail, audit string
|
|
32
|
+
if tab == 0 {
|
|
33
|
+
detail = styles.EpicItemFocused.Render("DETAIL [1]")
|
|
34
|
+
audit = styles.CardMeta.Render("AUDIT [2]")
|
|
35
|
+
} else {
|
|
36
|
+
detail = styles.CardMeta.Render("DETAIL [1]")
|
|
37
|
+
audit = styles.EpicItemFocused.Render("AUDIT [2]")
|
|
33
38
|
}
|
|
39
|
+
return detail + styles.CardMeta.Render(" │ ") + audit
|
|
40
|
+
}
|
|
34
41
|
|
|
42
|
+
// stripFrontmatter removes YAML frontmatter (between leading --- markers) from content.
|
|
43
|
+
func stripFrontmatter(content string) []string {
|
|
35
44
|
lines := strings.Split(content, "\n")
|
|
36
|
-
|
|
37
|
-
// Strip YAML frontmatter between leading --- markers.
|
|
38
45
|
start := 0
|
|
39
46
|
if len(lines) > 0 && strings.TrimSpace(lines[0]) == "---" {
|
|
40
47
|
for i := 1; i < len(lines); i++ {
|
|
@@ -44,7 +51,16 @@ func epicDetailBody(content string, width int) []string {
|
|
|
44
51
|
}
|
|
45
52
|
}
|
|
46
53
|
}
|
|
47
|
-
|
|
54
|
+
return lines[start:]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// epicDetailBody parses markdown content into display lines, stripping frontmatter.
|
|
58
|
+
func epicDetailBody(content string, width int) []string {
|
|
59
|
+
if strings.TrimSpace(content) == "" || content == "(no detail available)" {
|
|
60
|
+
return []string{styles.CardMeta.Render("(no detail available)")}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
lines := stripFrontmatter(content)
|
|
48
64
|
|
|
49
65
|
var body []string
|
|
50
66
|
skip := false
|
|
@@ -77,11 +93,87 @@ func epicDetailBody(content string, width int) []string {
|
|
|
77
93
|
return body
|
|
78
94
|
}
|
|
79
95
|
|
|
96
|
+
// RenderEpicAuditTab renders an overlay showing audit findings from an E##-Audit.md file.
|
|
97
|
+
func RenderEpicAuditTab(epicSlug, content string, overlayW, maxHeight, offset int, tab int) string {
|
|
98
|
+
inner := overlayW - detailBorderPad
|
|
99
|
+
if inner < 4 {
|
|
100
|
+
inner = 4
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
tabIndicator := renderTabIndicator(tab, inner)
|
|
104
|
+
lines := []string{
|
|
105
|
+
styles.GlyphAudit.Render("EPIC AUDIT"),
|
|
106
|
+
tabIndicator,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
body := epicAuditBody(content, inner)
|
|
110
|
+
body = append(body, "", styles.CardMeta.Render("1:Detail 2:Audit esc:close"))
|
|
111
|
+
lines = append(lines, visibleDetailLines(body, maxHeight-detailVerticalOverhead-1, offset)...)
|
|
112
|
+
|
|
113
|
+
return styles.EpicDetailOverlay.Width(overlayW).Render(strings.Join(lines, "\n"))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
var epicAuditHiddenSectionHeadings = map[string]struct{}{
|
|
117
|
+
"12. Distribution & build": {},
|
|
118
|
+
"Acceptance Criteria": {},
|
|
119
|
+
"Architectural notes": {},
|
|
120
|
+
"Boundaries": {},
|
|
121
|
+
"Context Files": {},
|
|
122
|
+
"Implemented As": {},
|
|
123
|
+
"Implemented as": {},
|
|
124
|
+
"Implementation Plan": {},
|
|
125
|
+
"Manual audit override": {},
|
|
126
|
+
"Proposed Changes": {},
|
|
127
|
+
"Quality Review": {},
|
|
128
|
+
"With": {},
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
func epicAuditBody(content string, width int) []string {
|
|
132
|
+
if strings.TrimSpace(content) == "" || content == "(no audit available)" {
|
|
133
|
+
return []string{styles.CardMeta.Render("(no audit available)")}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
lines := stripFrontmatter(content)
|
|
137
|
+
|
|
138
|
+
var body []string
|
|
139
|
+
inHiddenSection := false
|
|
140
|
+
|
|
141
|
+
for _, line := range lines {
|
|
142
|
+
trimmed := strings.TrimRight(line, " \t\r")
|
|
143
|
+
switch {
|
|
144
|
+
case strings.HasPrefix(trimmed, "## "):
|
|
145
|
+
sectionName := strings.TrimPrefix(trimmed, "## ")
|
|
146
|
+
_, inHiddenSection = epicAuditHiddenSectionHeadings[sectionName]
|
|
147
|
+
if !inHiddenSection {
|
|
148
|
+
body = append(body, "", styles.EpicItemFocused.Render(sectionName))
|
|
149
|
+
}
|
|
150
|
+
case inHiddenSection:
|
|
151
|
+
case strings.HasPrefix(trimmed, "### "):
|
|
152
|
+
body = append(body, styles.EpicItemFocused.Render(strings.TrimPrefix(trimmed, "### ")))
|
|
153
|
+
case strings.HasPrefix(trimmed, "- [x] ") || strings.HasPrefix(trimmed, "- [X] "):
|
|
154
|
+
text := strings.TrimPrefix(strings.TrimPrefix(trimmed, "- [x] "), "- [X] ")
|
|
155
|
+
body = append(body, renderChecklistSentences(text, "[x] ", width, styles.TagDone)...)
|
|
156
|
+
case strings.HasPrefix(trimmed, "- [ ] "):
|
|
157
|
+
text := strings.TrimPrefix(trimmed, "- [ ] ")
|
|
158
|
+
body = append(body, renderChecklistSentences(text, "[ ] ", width, styles.CardMeta)...)
|
|
159
|
+
case strings.HasPrefix(trimmed, "- "):
|
|
160
|
+
body = append(body, styles.CardMeta.Render("• "+strings.TrimPrefix(trimmed, "- ")))
|
|
161
|
+
case trimmed == "":
|
|
162
|
+
body = append(body, "")
|
|
163
|
+
default:
|
|
164
|
+
for _, wrapped := range WrapText(trimmed, width) {
|
|
165
|
+
body = append(body, styles.CardMeta.Render(wrapped))
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return body
|
|
170
|
+
}
|
|
171
|
+
|
|
80
172
|
const epicActiveMarker = "►"
|
|
81
173
|
|
|
82
174
|
// RenderEpicSidebar renders the fixed left sidebar listing epics with active indicator.
|
|
83
175
|
// If epics is empty and selected is non-empty, selected is shown as the sole entry.
|
|
84
|
-
func RenderEpicSidebar(epics []string, selected string, width int, focus bool, cursor int, status map[string]string) string {
|
|
176
|
+
func RenderEpicSidebar(epics []string, selected string, width int, focus bool, cursor int, status map[string]string, maxHeight int) string {
|
|
85
177
|
inner := width - epicPanelOverhead
|
|
86
178
|
if inner < 2 {
|
|
87
179
|
inner = 2
|
|
@@ -114,6 +206,20 @@ func RenderEpicSidebar(epics []string, selected string, width int, focus bool, c
|
|
|
114
206
|
if len(list) == 0 {
|
|
115
207
|
lines = append(lines, styles.TaskItem.Render("(none)"))
|
|
116
208
|
}
|
|
209
|
+
if maxHeight > 0 && len(lines) > maxHeight {
|
|
210
|
+
items := lines[2:]
|
|
211
|
+
available := maxHeight - 3
|
|
212
|
+
if available < 1 {
|
|
213
|
+
available = 1
|
|
214
|
+
}
|
|
215
|
+
clipped := make([]string, 0, maxHeight)
|
|
216
|
+
clipped = append(clipped, lines[0], lines[1])
|
|
217
|
+
clipped = append(clipped, items[:min(available, len(items))]...)
|
|
218
|
+
if len(items) > available {
|
|
219
|
+
clipped = append(clipped, renderScrollIndicator("↓", len(items)-available, "more"))
|
|
220
|
+
}
|
|
221
|
+
lines = clipped
|
|
222
|
+
}
|
|
117
223
|
style := styles.EpicPanel.Width(width)
|
|
118
224
|
if focus && len(epics) > 0 {
|
|
119
225
|
style = styles.EpicPanelFocused.Width(width)
|
|
@@ -154,13 +260,3 @@ func RenderEpicDropdown(epics []string, cursor int, width int) string {
|
|
|
154
260
|
lines = append(lines, "", styles.CardMeta.Render("↑↓:nav enter:select esc:cancel"))
|
|
155
261
|
return styles.EpicPanel.Width(width).Render(strings.Join(lines, "\n"))
|
|
156
262
|
}
|
|
157
|
-
|
|
158
|
-
// epicIndex returns the index of selected in epics, or 0 if not found.
|
|
159
|
-
func epicIndex(epics []string, selected string) int {
|
|
160
|
-
for i, e := range epics {
|
|
161
|
-
if e == selected {
|
|
162
|
-
return i
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return 0
|
|
166
|
-
}
|