savepoint 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +8 -1
- package/.savepoint/Design.md +26 -17
- package/.savepoint/audit/v1/E01/proposals.md +168 -0
- package/.savepoint/audit/v1/E01/snapshot.md +78 -0
- package/.savepoint/audit/{E01-go-setup → v1/E01-go-setup}/proposals.md +7 -7
- package/.savepoint/audit/{E01-go-setup → v1/E01-go-setup}/snapshot.md +2 -2
- package/.savepoint/audit/{E01-scaffolding → v1/E01-scaffolding}/proposals/AGENTS.md +5 -5
- package/.savepoint/audit/{E02-data-readers → v1/E02-data-readers}/proposals.md +20 -20
- package/.savepoint/audit/{E02-data-readers → v1/E02-data-readers}/snapshot.md +1 -1
- package/.savepoint/audit/{E03-board-tui-core → v1/E03-board-tui-core}/proposals.md +11 -11
- package/.savepoint/audit/{E03-board-tui-core → v1/E03-board-tui-core}/snapshot.md +1 -1
- package/.savepoint/audit/{E04-board-components → v1/E04-board-components}/proposals.md +14 -14
- package/.savepoint/audit/{E04-board-components → v1/E04-board-components}/snapshot.md +1 -1
- package/.savepoint/audit/{E05-init-command → v1/E05-init-command}/snapshot.md +1 -1
- package/.savepoint/audit/{E05-phase-transitions → v1/E05-phase-transitions}/proposals.md +4 -4
- package/.savepoint/audit/{E05-phase-transitions → v1/E05-phase-transitions}/snapshot.md +1 -1
- package/.savepoint/audit/v1/E06-atari-noir-layout/proposals.md +130 -0
- package/.savepoint/audit/v1/E06-atari-noir-layout/snapshot.md +84 -0
- package/.savepoint/audit/{E07-audit-pipeline → v1/E07-audit-pipeline}/snapshot.md +6 -6
- package/.savepoint/audit/v1.1/E02-cross-platform-compatibility/proposals.md +114 -0
- package/.savepoint/audit/v1.1/E02-cross-platform-compatibility/snapshot.md +41 -0
- package/.savepoint/audit/v1.1/E04-epic-navigation/proposals.md +156 -0
- package/.savepoint/audit/v1.1/E04-epic-navigation/snapshot.md +48 -0
- package/.savepoint/config.yml +3 -3
- 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/E06-Detail.md +62 -0
- 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/T007-detail-card-fixes.md +7 -7
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T008-checkbox-states.md +10 -8
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T009-router-priority-marker.md +16 -9
- package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T010-auto-refresh-watcher.md +27 -22
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/E01-Detail.md +40 -0
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T001-next-activity-header.md +56 -0
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T002-rename-epic-design-files.md +38 -0
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T003-rename-release-prd.md +28 -0
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T004-update-instruction-files.md +51 -0
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T005-update-cross-references.md +45 -0
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T006-column-and-detail-scrolling.md +68 -0
- 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-Detail.md +49 -0
- package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T001-fix-makefile.md +37 -0
- package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T002-linux-build-target.md +38 -0
- package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T003-macos-build-target.md +36 -0
- package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T004-smoke-tests-and-artifacts.md +59 -0
- package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/E03-Detail.md +32 -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-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-Detail.md +45 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T001-update-agents-md.md +34 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T002-update-router-md.md +30 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T003-update-design-md.md +33 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T004-implement-m-hotkey.md +88 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T005-update-help-overlay.md +30 -0
- package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T006-tests-and-quality-gates.md +46 -0
- package/.savepoint/releases/v1.1/v1.1-PRD.md +79 -0
- package/.savepoint/router.md +33 -105
- package/.savepoint/visual-identity.md +4 -3
- package/AGENTS.md +56 -113
- package/Makefile +19 -3
- package/README.md +7 -6
- package/agent-skills/savepoint-audit/SKILL.md +6 -6
- package/agent-skills/savepoint-build-task/SKILL.md +2 -2
- package/agent-skills/savepoint-create-plan/SKILL.md +3 -3
- package/agent-skills/savepoint-create-task/SKILL.md +2 -2
- package/agent-skills/savepoint-draft-prd/SKILL.md +1 -1
- package/agent-skills/savepoint-system-design/SKILL.md +1 -1
- package/go.mod +4 -1
- package/go.sum +2 -0
- package/internal/board/board.go +66 -14
- package/internal/board/board_test.go +124 -0
- package/internal/board/card.go +40 -3
- package/internal/board/card_test.go +121 -14
- package/internal/board/column.go +40 -5
- package/internal/board/column_test.go +65 -10
- package/internal/board/detail.go +115 -23
- package/internal/board/detail_test.go +132 -25
- package/internal/board/epic_panel.go +105 -8
- package/internal/board/epic_panel_test.go +343 -5
- package/internal/board/layout.go +12 -2
- package/internal/board/layout_test.go +17 -0
- package/internal/board/model.go +146 -23
- package/internal/board/render_policy_test.go +77 -0
- package/internal/board/status.go +23 -0
- package/internal/board/update.go +300 -9
- package/internal/board/update_test.go +166 -0
- package/internal/board/view.go +141 -17
- package/internal/board/view_test.go +161 -3
- package/internal/board/watch.go +100 -0
- package/internal/buildtool/main.go +219 -0
- package/internal/data/parser.go +39 -1
- package/internal/data/parser_test.go +43 -2
- package/internal/data/task.go +22 -2
- package/internal/styles/palette.go +9 -7
- package/internal/styles/styles.go +42 -25
- package/main.go +9 -0
- package/package.json +5 -4
- package/savepoint +0 -0
- package/savepoint.exe +0 -0
- package/templates/project/.savepoint/router.md +6 -5
- package/templates/project/AGENTS.md +47 -101
- package/templates/prompts/audit-reconciliation.prompt.md +6 -6
- 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/releases/v1/epics/E06-atari-noir-layout/Design.md +0 -42
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/Design.md +0 -26
- package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T001-border-resize-fix.md +0 -35
- package/main.exe +0 -0
- /package/.savepoint/audit/{E01-scaffolding → v1/E01-scaffolding}/proposals/Design.md +0 -0
- /package/.savepoint/audit/{E01-scaffolding → v1/E01-scaffolding}/proposals/epic-Design.md +0 -0
- /package/.savepoint/audit/{E01-scaffolding → v1/E01-scaffolding}/proposals/quality-review.md +0 -0
- /package/.savepoint/audit/{E01-scaffolding → v1/E01-scaffolding}/snapshot.md +0 -0
- /package/.savepoint/audit/{E02-data-model → v1/E02-data-model}/snapshot.md +0 -0
- /package/.savepoint/audit/{E03-cli-foundation → v1/E03-cli-foundation}/snapshot.md +0 -0
- /package/.savepoint/audit/{E04-templates-and-prompts → v1/E04-templates-and-prompts}/snapshot.md +0 -0
- /package/.savepoint/audit/{E06-tui-board → v1/E06-tui-board}/snapshot.md +0 -0
- /package/.savepoint/audit/{E08-board-workflow-cleanup → v1/E08-board-workflow-cleanup}/snapshot.md +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
|
@@ -5,37 +5,49 @@ import (
|
|
|
5
5
|
"path/filepath"
|
|
6
6
|
|
|
7
7
|
tea "github.com/charmbracelet/bubbletea"
|
|
8
|
+
"github.com/fsnotify/fsnotify"
|
|
8
9
|
"github.com/opencode/savepoint/internal/data"
|
|
9
10
|
)
|
|
10
11
|
|
|
11
12
|
type OverlayType string
|
|
12
13
|
|
|
13
14
|
const (
|
|
14
|
-
OverlayNone
|
|
15
|
-
OverlayHelp
|
|
16
|
-
OverlayEpic
|
|
17
|
-
OverlayRelease
|
|
18
|
-
OverlayDetail
|
|
15
|
+
OverlayNone OverlayType = ""
|
|
16
|
+
OverlayHelp OverlayType = "help"
|
|
17
|
+
OverlayEpic OverlayType = "epic"
|
|
18
|
+
OverlayRelease OverlayType = "release"
|
|
19
|
+
OverlayDetail OverlayType = "detail"
|
|
20
|
+
OverlayEpicDetail OverlayType = "detail-epic"
|
|
19
21
|
)
|
|
20
22
|
|
|
21
23
|
// Model holds all board state. Tasks are grouped by column for O(1) column access.
|
|
22
24
|
type Model struct {
|
|
23
|
-
AllTasks
|
|
24
|
-
Tasks
|
|
25
|
-
FocusedColumn
|
|
26
|
-
FocusedTask
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
25
|
+
AllTasks []data.Task
|
|
26
|
+
Tasks map[data.ColumnType][]data.Task
|
|
27
|
+
FocusedColumn data.ColumnType
|
|
28
|
+
FocusedTask int
|
|
29
|
+
ColumnOffsets map[data.ColumnType]int
|
|
30
|
+
DetailOffset int
|
|
31
|
+
SelectedEpic string
|
|
32
|
+
SelectedRelease string
|
|
33
|
+
Epics []string
|
|
34
|
+
EpicCursor int
|
|
35
|
+
EpicPanelFocus bool
|
|
36
|
+
EpicPanelCursor int
|
|
37
|
+
EpicDetailOffset int
|
|
38
|
+
EpicDetailContent string
|
|
39
|
+
Releases []string
|
|
40
|
+
ReleaseEpics map[string][]string
|
|
41
|
+
ReleaseCursor int
|
|
42
|
+
Overlay OverlayType
|
|
43
|
+
Width int
|
|
44
|
+
Height int
|
|
45
|
+
StatusMessage string
|
|
46
|
+
Root string
|
|
47
|
+
EpicStatus map[string]string
|
|
48
|
+
RouterTask string
|
|
49
|
+
RouterState *data.RouterState
|
|
50
|
+
Watcher *fsnotify.Watcher
|
|
39
51
|
}
|
|
40
52
|
|
|
41
53
|
// NewModel groups tasks by column and returns an initialized Model.
|
|
@@ -44,6 +56,7 @@ func NewModel(tasks []data.Task, release, epic string) Model {
|
|
|
44
56
|
AllTasks: append([]data.Task(nil), tasks...),
|
|
45
57
|
FocusedColumn: data.ColumnPlanned,
|
|
46
58
|
FocusedTask: 0,
|
|
59
|
+
ColumnOffsets: newColumnOffsets(),
|
|
47
60
|
SelectedEpic: epic,
|
|
48
61
|
SelectedRelease: release,
|
|
49
62
|
Overlay: OverlayNone,
|
|
@@ -53,7 +66,10 @@ func NewModel(tasks []data.Task, release, epic string) Model {
|
|
|
53
66
|
}
|
|
54
67
|
|
|
55
68
|
func (m Model) Init() tea.Cmd {
|
|
56
|
-
|
|
69
|
+
if m.Watcher == nil {
|
|
70
|
+
return nil
|
|
71
|
+
}
|
|
72
|
+
return watchFiles(m.Watcher)
|
|
57
73
|
}
|
|
58
74
|
|
|
59
75
|
func groupedTasks(tasks []data.Task) map[data.ColumnType][]data.Task {
|
|
@@ -85,6 +101,15 @@ func (m *Model) refreshTasks() {
|
|
|
85
101
|
}
|
|
86
102
|
m.Tasks = groupedTasks(visible)
|
|
87
103
|
m.clampFocusedTask()
|
|
104
|
+
m.clampColumnOffsets()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
func newColumnOffsets() map[data.ColumnType]int {
|
|
108
|
+
return map[data.ColumnType]int{
|
|
109
|
+
data.ColumnPlanned: 0,
|
|
110
|
+
data.ColumnInProgress: 0,
|
|
111
|
+
data.ColumnDone: 0,
|
|
112
|
+
}
|
|
88
113
|
}
|
|
89
114
|
|
|
90
115
|
func (m *Model) refreshEpicsForRelease() {
|
|
@@ -97,18 +122,36 @@ func (m *Model) refreshEpicsForRelease() {
|
|
|
97
122
|
if len(m.Epics) == 0 {
|
|
98
123
|
m.SelectedEpic = ""
|
|
99
124
|
m.EpicCursor = 0
|
|
125
|
+
m.EpicPanelCursor = 0
|
|
126
|
+
m.EpicPanelFocus = false
|
|
100
127
|
return
|
|
101
128
|
}
|
|
102
129
|
|
|
103
130
|
for _, epic := range m.Epics {
|
|
104
131
|
if epic == m.SelectedEpic {
|
|
105
132
|
m.EpicCursor = epicIndex(m.Epics, m.SelectedEpic)
|
|
133
|
+
m.clampEpicPanelCursor()
|
|
106
134
|
return
|
|
107
135
|
}
|
|
108
136
|
}
|
|
109
137
|
|
|
110
138
|
m.SelectedEpic = m.Epics[0]
|
|
111
139
|
m.EpicCursor = 0
|
|
140
|
+
m.clampEpicPanelCursor()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func (m *Model) clampEpicPanelCursor() {
|
|
144
|
+
if len(m.Epics) == 0 {
|
|
145
|
+
m.EpicPanelCursor = 0
|
|
146
|
+
m.EpicPanelFocus = false
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
if m.EpicPanelCursor >= len(m.Epics) {
|
|
150
|
+
m.EpicPanelCursor = len(m.Epics) - 1
|
|
151
|
+
}
|
|
152
|
+
if m.EpicPanelCursor < 0 {
|
|
153
|
+
m.EpicPanelCursor = 0
|
|
154
|
+
}
|
|
112
155
|
}
|
|
113
156
|
|
|
114
157
|
func (m *Model) clampFocusedTask() {
|
|
@@ -125,6 +168,23 @@ func (m *Model) clampFocusedTask() {
|
|
|
125
168
|
}
|
|
126
169
|
}
|
|
127
170
|
|
|
171
|
+
func (m *Model) clampColumnOffsets() {
|
|
172
|
+
if m.ColumnOffsets == nil {
|
|
173
|
+
m.ColumnOffsets = newColumnOffsets()
|
|
174
|
+
}
|
|
175
|
+
for _, col := range columnOrder {
|
|
176
|
+
tasks := m.Tasks[col]
|
|
177
|
+
offset := m.ColumnOffsets[col]
|
|
178
|
+
if offset < 0 || len(tasks) == 0 {
|
|
179
|
+
m.ColumnOffsets[col] = 0
|
|
180
|
+
continue
|
|
181
|
+
}
|
|
182
|
+
if offset >= len(tasks) {
|
|
183
|
+
m.ColumnOffsets[col] = len(tasks) - 1
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
128
188
|
func (m *Model) writeRouterReleaseEpic() error {
|
|
129
189
|
routerPath := filepath.Join(m.Root, "router.md")
|
|
130
190
|
|
|
@@ -144,8 +204,71 @@ func (m *Model) writeRouterReleaseEpic() error {
|
|
|
144
204
|
return err
|
|
145
205
|
}
|
|
146
206
|
|
|
147
|
-
state.Epic = m.SelectedEpic
|
|
207
|
+
state.Epic = shortID(m.SelectedEpic)
|
|
148
208
|
state.Release = m.SelectedRelease
|
|
149
209
|
|
|
150
210
|
return data.WriteRouterState(m.Root, state, fi.ModTime())
|
|
151
211
|
}
|
|
212
|
+
|
|
213
|
+
func (m *Model) writeRouterTask(task data.Task) (string, error) {
|
|
214
|
+
routerPath := filepath.Join(m.Root, "router.md")
|
|
215
|
+
|
|
216
|
+
fi, err := os.Stat(routerPath)
|
|
217
|
+
if err != nil {
|
|
218
|
+
return "", err
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
content, err := os.ReadFile(routerPath)
|
|
222
|
+
if err != nil {
|
|
223
|
+
return "", err
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
r := data.NewRouterReader()
|
|
227
|
+
state, err := r.ReadState(string(content))
|
|
228
|
+
if err != nil {
|
|
229
|
+
return "", err
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
state.Release = task.Release
|
|
233
|
+
state.Epic = task.Epic
|
|
234
|
+
if m.isLastUncompletedTask(task) {
|
|
235
|
+
state.State = "audit-pending"
|
|
236
|
+
state.Task = ""
|
|
237
|
+
state.NextAction = "Audit " + task.Epic + "."
|
|
238
|
+
if err := data.WriteRouterState(m.Root, state, fi.ModTime()); err != nil {
|
|
239
|
+
return "", err
|
|
240
|
+
}
|
|
241
|
+
m.RouterState = state
|
|
242
|
+
m.RouterTask = ""
|
|
243
|
+
return "Audit pending for " + task.Epic, nil
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
state.State = "task-building"
|
|
247
|
+
state.Task = task.ID
|
|
248
|
+
state.NextAction = "Build " + task.ID + "."
|
|
249
|
+
if err := data.WriteRouterState(m.Root, state, fi.ModTime()); err != nil {
|
|
250
|
+
return "", err
|
|
251
|
+
}
|
|
252
|
+
m.RouterState = state
|
|
253
|
+
m.RouterTask = task.ID
|
|
254
|
+
return "Router set to " + task.Release + " " + task.Epic + "/" + shortID(task.ID), nil
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
func (m Model) isLastUncompletedTask(task data.Task) bool {
|
|
258
|
+
for _, candidate := range m.AllTasks {
|
|
259
|
+
if candidate.ID == task.ID {
|
|
260
|
+
continue
|
|
261
|
+
}
|
|
262
|
+
if candidate.Release != task.Release || candidate.Epic != task.Epic {
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
if !taskDone(candidate) {
|
|
266
|
+
return false
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return true
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
func taskDone(task data.Task) bool {
|
|
273
|
+
return task.Column == data.ColumnDone || task.Status == string(data.StatusDone)
|
|
274
|
+
}
|
|
@@ -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.StatusPlanned):
|
|
13
|
+
return styles.CardMeta.Render("○")
|
|
14
|
+
case string(data.StatusInProgress):
|
|
15
|
+
return styles.GlyphBuild.Render("▶")
|
|
16
|
+
case string(data.StatusDone):
|
|
17
|
+
return styles.TagDone.Render("◉")
|
|
18
|
+
case string(data.StatusAudited):
|
|
19
|
+
return styles.TagDone.Render("✓")
|
|
20
|
+
default:
|
|
21
|
+
return statusGlyphDefault
|
|
22
|
+
}
|
|
23
|
+
}
|