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.
Files changed (136) hide show
  1. package/.claude/settings.local.json +8 -1
  2. package/.savepoint/Design.md +26 -17
  3. package/.savepoint/audit/v1/E01/proposals.md +168 -0
  4. package/.savepoint/audit/v1/E01/snapshot.md +78 -0
  5. package/.savepoint/audit/{E01-go-setup → v1/E01-go-setup}/proposals.md +7 -7
  6. package/.savepoint/audit/{E01-go-setup → v1/E01-go-setup}/snapshot.md +2 -2
  7. package/.savepoint/audit/{E01-scaffolding → v1/E01-scaffolding}/proposals/AGENTS.md +5 -5
  8. package/.savepoint/audit/{E02-data-readers → v1/E02-data-readers}/proposals.md +20 -20
  9. package/.savepoint/audit/{E02-data-readers → v1/E02-data-readers}/snapshot.md +1 -1
  10. package/.savepoint/audit/{E03-board-tui-core → v1/E03-board-tui-core}/proposals.md +11 -11
  11. package/.savepoint/audit/{E03-board-tui-core → v1/E03-board-tui-core}/snapshot.md +1 -1
  12. package/.savepoint/audit/{E04-board-components → v1/E04-board-components}/proposals.md +14 -14
  13. package/.savepoint/audit/{E04-board-components → v1/E04-board-components}/snapshot.md +1 -1
  14. package/.savepoint/audit/{E05-init-command → v1/E05-init-command}/snapshot.md +1 -1
  15. package/.savepoint/audit/{E05-phase-transitions → v1/E05-phase-transitions}/proposals.md +4 -4
  16. package/.savepoint/audit/{E05-phase-transitions → v1/E05-phase-transitions}/snapshot.md +1 -1
  17. package/.savepoint/audit/v1/E06-atari-noir-layout/proposals.md +130 -0
  18. package/.savepoint/audit/v1/E06-atari-noir-layout/snapshot.md +84 -0
  19. package/.savepoint/audit/{E07-audit-pipeline → v1/E07-audit-pipeline}/snapshot.md +6 -6
  20. package/.savepoint/audit/v1.1/E02-cross-platform-compatibility/proposals.md +114 -0
  21. package/.savepoint/audit/v1.1/E02-cross-platform-compatibility/snapshot.md +41 -0
  22. package/.savepoint/audit/v1.1/E04-epic-navigation/proposals.md +156 -0
  23. package/.savepoint/audit/v1.1/E04-epic-navigation/snapshot.md +48 -0
  24. package/.savepoint/config.yml +3 -3
  25. package/.savepoint/releases/v1/epics/E01-go-setup/tasks/T001-init-module.md +1 -1
  26. package/.savepoint/releases/v1/epics/E03-board-tui-core/tasks/T005-layout.md +1 -1
  27. package/.savepoint/releases/v1/epics/E04-board-components/tasks/T002-card.md +1 -1
  28. package/.savepoint/releases/v1/epics/E04-board-components/tasks/T006-help-overlay.md +1 -1
  29. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/E06-Detail.md +62 -0
  30. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T002-header-and-dividers.md +1 -1
  31. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T003-footer-status-bar.md +1 -1
  32. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T004-component-refinement.md +1 -1
  33. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T007-detail-card-fixes.md +7 -7
  34. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T008-checkbox-states.md +10 -8
  35. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T009-router-priority-marker.md +16 -9
  36. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/tasks/T010-auto-refresh-watcher.md +27 -22
  37. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/E01-Detail.md +40 -0
  38. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T001-next-activity-header.md +56 -0
  39. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T002-rename-epic-design-files.md +38 -0
  40. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T003-rename-release-prd.md +28 -0
  41. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T004-update-instruction-files.md +51 -0
  42. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T005-update-cross-references.md +45 -0
  43. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T006-column-and-detail-scrolling.md +68 -0
  44. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T007-column-focus-border-stability.md +57 -0
  45. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/E02-Detail.md +49 -0
  46. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T001-fix-makefile.md +37 -0
  47. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T002-linux-build-target.md +38 -0
  48. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T003-macos-build-target.md +36 -0
  49. package/.savepoint/releases/v1.1/epics/E02-cross-platform-compatibility/tasks/T004-smoke-tests-and-artifacts.md +59 -0
  50. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/E03-Detail.md +32 -0
  51. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T001-border-resize-fix.md +40 -0
  52. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T002-next-activity-below-header.md +64 -0
  53. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T003-checkbox-rendering-fix.md +56 -0
  54. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T005-unify-status-glyphs.md +65 -0
  55. package/.savepoint/releases/v1.1/epics/E03-ui-visual-refinement/tasks/T006-forced-256-color-profile.md +36 -0
  56. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/E04-Detail.md +51 -0
  57. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T001-sidebar-focusable-navigation.md +65 -0
  58. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T002-epic-detail-overlay.md +73 -0
  59. package/.savepoint/releases/v1.1/epics/E04-epic-navigation/tasks/T003-epic-status-glyphs.md +73 -0
  60. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/E05-Detail.md +45 -0
  61. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T001-update-agents-md.md +34 -0
  62. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T002-update-router-md.md +30 -0
  63. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T003-update-design-md.md +33 -0
  64. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T004-implement-m-hotkey.md +88 -0
  65. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T005-update-help-overlay.md +30 -0
  66. package/.savepoint/releases/v1.1/epics/E05-tasking-permissions/tasks/T006-tests-and-quality-gates.md +46 -0
  67. package/.savepoint/releases/v1.1/v1.1-PRD.md +79 -0
  68. package/.savepoint/router.md +33 -105
  69. package/.savepoint/visual-identity.md +4 -3
  70. package/AGENTS.md +56 -113
  71. package/Makefile +19 -3
  72. package/README.md +7 -6
  73. package/agent-skills/savepoint-audit/SKILL.md +6 -6
  74. package/agent-skills/savepoint-build-task/SKILL.md +2 -2
  75. package/agent-skills/savepoint-create-plan/SKILL.md +3 -3
  76. package/agent-skills/savepoint-create-task/SKILL.md +2 -2
  77. package/agent-skills/savepoint-draft-prd/SKILL.md +1 -1
  78. package/agent-skills/savepoint-system-design/SKILL.md +1 -1
  79. package/go.mod +4 -1
  80. package/go.sum +2 -0
  81. package/internal/board/board.go +66 -14
  82. package/internal/board/board_test.go +124 -0
  83. package/internal/board/card.go +40 -3
  84. package/internal/board/card_test.go +121 -14
  85. package/internal/board/column.go +40 -5
  86. package/internal/board/column_test.go +65 -10
  87. package/internal/board/detail.go +115 -23
  88. package/internal/board/detail_test.go +132 -25
  89. package/internal/board/epic_panel.go +105 -8
  90. package/internal/board/epic_panel_test.go +343 -5
  91. package/internal/board/layout.go +12 -2
  92. package/internal/board/layout_test.go +17 -0
  93. package/internal/board/model.go +146 -23
  94. package/internal/board/render_policy_test.go +77 -0
  95. package/internal/board/status.go +23 -0
  96. package/internal/board/update.go +300 -9
  97. package/internal/board/update_test.go +166 -0
  98. package/internal/board/view.go +141 -17
  99. package/internal/board/view_test.go +161 -3
  100. package/internal/board/watch.go +100 -0
  101. package/internal/buildtool/main.go +219 -0
  102. package/internal/data/parser.go +39 -1
  103. package/internal/data/parser_test.go +43 -2
  104. package/internal/data/task.go +22 -2
  105. package/internal/styles/palette.go +9 -7
  106. package/internal/styles/styles.go +42 -25
  107. package/main.go +9 -0
  108. package/package.json +5 -4
  109. package/savepoint +0 -0
  110. package/savepoint.exe +0 -0
  111. package/templates/project/.savepoint/router.md +6 -5
  112. package/templates/project/AGENTS.md +47 -101
  113. package/templates/prompts/audit-reconciliation.prompt.md +6 -6
  114. package/templates/prompts/epic-design.prompt.md +3 -3
  115. package/templates/prompts/task-breakdown.prompt.md +1 -1
  116. package/templates/prompts/task-building.prompt.md +1 -1
  117. package/templates/prompts/task-planning.prompt.md +1 -1
  118. package/.savepoint/releases/v1/epics/E06-atari-noir-layout/Design.md +0 -42
  119. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/Design.md +0 -26
  120. package/.savepoint/releases/v1.1/epics/E01-tui-optimisation/tasks/T001-border-resize-fix.md +0 -35
  121. package/main.exe +0 -0
  122. /package/.savepoint/audit/{E01-scaffolding → v1/E01-scaffolding}/proposals/Design.md +0 -0
  123. /package/.savepoint/audit/{E01-scaffolding → v1/E01-scaffolding}/proposals/epic-Design.md +0 -0
  124. /package/.savepoint/audit/{E01-scaffolding → v1/E01-scaffolding}/proposals/quality-review.md +0 -0
  125. /package/.savepoint/audit/{E01-scaffolding → v1/E01-scaffolding}/snapshot.md +0 -0
  126. /package/.savepoint/audit/{E02-data-model → v1/E02-data-model}/snapshot.md +0 -0
  127. /package/.savepoint/audit/{E03-cli-foundation → v1/E03-cli-foundation}/snapshot.md +0 -0
  128. /package/.savepoint/audit/{E04-templates-and-prompts → v1/E04-templates-and-prompts}/snapshot.md +0 -0
  129. /package/.savepoint/audit/{E06-tui-board → v1/E06-tui-board}/snapshot.md +0 -0
  130. /package/.savepoint/audit/{E08-board-workflow-cleanup → v1/E08-board-workflow-cleanup}/snapshot.md +0 -0
  131. /package/.savepoint/releases/v1/epics/E01-go-setup/{Design.md → E01-Detail.md} +0 -0
  132. /package/.savepoint/releases/v1/epics/E02-data-readers/{Design.md → E02-Detail.md} +0 -0
  133. /package/.savepoint/releases/v1/epics/E03-board-tui-core/{Design.md → E03-Detail.md} +0 -0
  134. /package/.savepoint/releases/v1/epics/E04-board-components/{Design.md → E04-Detail.md} +0 -0
  135. /package/.savepoint/releases/v1/epics/E05-phase-transitions/{Design.md → E05-Detail.md} +0 -0
  136. /package/.savepoint/releases/v1/{PRD.md → v1-PRD.md} +0 -0
@@ -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 OverlayType = ""
15
- OverlayHelp OverlayType = "help"
16
- OverlayEpic OverlayType = "epic"
17
- OverlayRelease OverlayType = "release"
18
- OverlayDetail OverlayType = "detail"
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 []data.Task
24
- Tasks map[data.ColumnType][]data.Task
25
- FocusedColumn data.ColumnType
26
- FocusedTask int
27
- SelectedEpic string
28
- SelectedRelease string
29
- Epics []string
30
- EpicCursor int
31
- Releases []string
32
- ReleaseEpics map[string][]string
33
- ReleaseCursor int
34
- Overlay OverlayType
35
- Width int
36
- Height int
37
- StatusMessage string
38
- Root string
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
- return tea.Batch()
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
+ }