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/detail.go
CHANGED
|
@@ -3,15 +3,17 @@ package board
|
|
|
3
3
|
import (
|
|
4
4
|
"strings"
|
|
5
5
|
|
|
6
|
+
"github.com/charmbracelet/lipgloss"
|
|
6
7
|
"github.com/opencode/savepoint/internal/data"
|
|
7
8
|
"github.com/opencode/savepoint/internal/styles"
|
|
8
9
|
)
|
|
9
10
|
|
|
10
|
-
const detailBorderPad = 4
|
|
11
|
+
const detailBorderPad = 4 // rounded border (2) + padding (2×1)
|
|
12
|
+
const detailVerticalOverhead = 4 // overlay border (2) + fixed title/header rows (2)
|
|
11
13
|
|
|
12
14
|
// RenderDetail renders a task detail overlay panel at the given display width.
|
|
13
|
-
// When
|
|
14
|
-
func RenderDetail(t data.Task, overlayW int,
|
|
15
|
+
// When router state matches t's release/epic/task, a "(router priority)" label is shown.
|
|
16
|
+
func RenderDetail(t data.Task, overlayW int, routerState *data.RouterState, maxHeight, offset int) string {
|
|
15
17
|
inner := overlayW - detailBorderPad
|
|
16
18
|
if inner < 4 {
|
|
17
19
|
inner = 4
|
|
@@ -21,57 +23,137 @@ func RenderDetail(t data.Task, overlayW int, routerTaskID string) string {
|
|
|
21
23
|
styles.ColumnTitleFocused.Render("TASK DETAIL"),
|
|
22
24
|
strings.Repeat("─", inner),
|
|
23
25
|
}
|
|
24
|
-
|
|
26
|
+
body := []string{
|
|
25
27
|
detailRow("ID", t.ID, inner),
|
|
26
28
|
detailRow("Title", t.Title, inner),
|
|
27
29
|
detailRow("Epic", t.Epic, inner),
|
|
28
30
|
detailRow("Release", t.Release, inner),
|
|
29
31
|
detailRow("Status", string(t.Column), inner),
|
|
30
32
|
detailRow("Phase", phaseLabel(t.Stage), inner),
|
|
31
|
-
|
|
33
|
+
}
|
|
32
34
|
|
|
33
35
|
if t.Description != "" {
|
|
34
|
-
|
|
36
|
+
body = append(body,
|
|
35
37
|
"",
|
|
36
38
|
styles.ColumnTitle.Render("Description:"),
|
|
37
39
|
)
|
|
38
40
|
for _, line := range WrapText(t.Description, inner) {
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if len(t.Acceptance) > 0 {
|
|
44
|
-
lines = append(lines, "", styles.ColumnTitle.Render("Acceptance Criteria:"), "")
|
|
45
|
-
for _, a := range t.Acceptance {
|
|
46
|
-
for _, line := range WrapText(a, inner-2) {
|
|
47
|
-
lines = append(lines, styles.CardMeta.Render(" • "+line))
|
|
48
|
-
}
|
|
41
|
+
body = append(body, styles.CardMeta.Render(line))
|
|
49
42
|
}
|
|
50
43
|
}
|
|
51
44
|
|
|
52
45
|
if len(t.Checklist) > 0 {
|
|
53
|
-
|
|
46
|
+
body = append(body, "", styles.ColumnTitle.Render("Implementation Plan:"), "")
|
|
54
47
|
for _, item := range t.Checklist {
|
|
55
|
-
glyph := "
|
|
48
|
+
glyph := "[ ] "
|
|
56
49
|
style := styles.CardMeta
|
|
57
50
|
if item.Done {
|
|
58
|
-
glyph = "
|
|
51
|
+
glyph = "[x] "
|
|
59
52
|
style = styles.TagDone
|
|
60
53
|
}
|
|
61
|
-
|
|
62
|
-
lines = append(lines, style.Render(" "+glyph+line))
|
|
63
|
-
}
|
|
54
|
+
body = append(body, renderChecklistSentences(item.Text, glyph, inner, style)...)
|
|
64
55
|
}
|
|
65
56
|
}
|
|
66
57
|
|
|
67
|
-
if
|
|
68
|
-
|
|
58
|
+
if t.Column != data.ColumnDone && isRouterPriority(t, routerState) {
|
|
59
|
+
body = append(body, "", styles.TagDone.Render("(router priority)"))
|
|
69
60
|
}
|
|
70
|
-
|
|
61
|
+
body = append(body, "", styles.CardMeta.Render("esc:close"))
|
|
62
|
+
lines = append(lines, visibleDetailLines(body, maxHeight-detailVerticalOverhead, offset)...)
|
|
71
63
|
|
|
72
64
|
return styles.DetailOverlay.Width(overlayW).Render(strings.Join(lines, "\n"))
|
|
73
65
|
}
|
|
74
66
|
|
|
67
|
+
func renderChecklistSentences(text, glyph string, width int, style lipgloss.Style) []string {
|
|
68
|
+
textWidth := width - len(glyph)
|
|
69
|
+
if textWidth < 4 {
|
|
70
|
+
textWidth = 4
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
lines := []string{}
|
|
74
|
+
continuationIndent := strings.Repeat(" ", len(glyph))
|
|
75
|
+
for _, sentence := range splitChecklistSentences(text) {
|
|
76
|
+
wrapped := WrapText(sentence, textWidth)
|
|
77
|
+
for i, line := range wrapped {
|
|
78
|
+
if i == 0 {
|
|
79
|
+
lines = append(lines, style.Render(glyph+line))
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
lines = append(lines, style.Render(continuationIndent+line))
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return lines
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func splitChecklistSentences(text string) []string {
|
|
89
|
+
fields := strings.Fields(text)
|
|
90
|
+
if len(fields) == 0 {
|
|
91
|
+
return nil
|
|
92
|
+
}
|
|
93
|
+
normalized := strings.Join(fields, " ")
|
|
94
|
+
|
|
95
|
+
sentences := []string{}
|
|
96
|
+
start := 0
|
|
97
|
+
for i, r := range normalized {
|
|
98
|
+
if r != '.' && r != '!' && r != '?' {
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
end := i + len(string(r))
|
|
102
|
+
if end < len(normalized) && normalized[end] != ' ' {
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
sentence := strings.TrimSpace(normalized[start:end])
|
|
106
|
+
if sentence != "" {
|
|
107
|
+
sentences = append(sentences, sentence)
|
|
108
|
+
}
|
|
109
|
+
start = end
|
|
110
|
+
}
|
|
111
|
+
if tail := strings.TrimSpace(normalized[start:]); tail != "" {
|
|
112
|
+
sentences = append(sentences, tail)
|
|
113
|
+
}
|
|
114
|
+
return sentences
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
func visibleDetailLines(lines []string, maxBodyHeight, offset int) []string {
|
|
118
|
+
total := len(lines)
|
|
119
|
+
if maxBodyHeight <= 0 || total <= maxBodyHeight {
|
|
120
|
+
return lines
|
|
121
|
+
}
|
|
122
|
+
offset = clampDetailOffset(offset, total)
|
|
123
|
+
available := maxBodyHeight
|
|
124
|
+
if offset > 0 {
|
|
125
|
+
available--
|
|
126
|
+
}
|
|
127
|
+
if available < 1 {
|
|
128
|
+
available = 1
|
|
129
|
+
}
|
|
130
|
+
end := min(offset+available, total)
|
|
131
|
+
if end < total && available > 1 {
|
|
132
|
+
available--
|
|
133
|
+
end = min(offset+available, total)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
visible := make([]string, 0, available+2)
|
|
137
|
+
if offset > 0 {
|
|
138
|
+
visible = append(visible, renderScrollIndicator("↑", offset, "above"))
|
|
139
|
+
}
|
|
140
|
+
visible = append(visible, lines[offset:end]...)
|
|
141
|
+
if end < total {
|
|
142
|
+
visible = append(visible, renderScrollIndicator("↓", total-end, "more"))
|
|
143
|
+
}
|
|
144
|
+
return visible
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
func clampDetailOffset(offset, total int) int {
|
|
148
|
+
if offset < 0 || total <= 0 {
|
|
149
|
+
return 0
|
|
150
|
+
}
|
|
151
|
+
if offset >= total {
|
|
152
|
+
return total - 1
|
|
153
|
+
}
|
|
154
|
+
return offset
|
|
155
|
+
}
|
|
156
|
+
|
|
75
157
|
func detailRow(label, value string, width int) string {
|
|
76
158
|
prefix := label + ": "
|
|
77
159
|
wrapped := WrapText(value, width-len(prefix))
|
|
@@ -100,51 +182,4 @@ func phaseLabel(s data.ProgressStage) string {
|
|
|
100
182
|
}
|
|
101
183
|
}
|
|
102
184
|
|
|
103
|
-
func WrapText(s string, width int) []string {
|
|
104
|
-
if width < 4 {
|
|
105
|
-
width = 4
|
|
106
|
-
}
|
|
107
|
-
words := strings.Fields(s)
|
|
108
|
-
if len(words) == 0 {
|
|
109
|
-
return nil
|
|
110
|
-
}
|
|
111
|
-
lines := []string{}
|
|
112
|
-
current := ""
|
|
113
|
-
for _, word := range words {
|
|
114
|
-
if len([]rune(word)) > width {
|
|
115
|
-
if current != "" {
|
|
116
|
-
lines = append(lines, current)
|
|
117
|
-
current = ""
|
|
118
|
-
}
|
|
119
|
-
lines = append(lines, SplitLongWord(word, width)...)
|
|
120
|
-
continue
|
|
121
|
-
}
|
|
122
|
-
if current == "" {
|
|
123
|
-
current = word
|
|
124
|
-
continue
|
|
125
|
-
}
|
|
126
|
-
if len([]rune(current))+1+len([]rune(word)) <= width {
|
|
127
|
-
current += " " + word
|
|
128
|
-
continue
|
|
129
|
-
}
|
|
130
|
-
lines = append(lines, current)
|
|
131
|
-
current = word
|
|
132
|
-
}
|
|
133
|
-
if current != "" {
|
|
134
|
-
lines = append(lines, current)
|
|
135
|
-
}
|
|
136
|
-
return lines
|
|
137
|
-
}
|
|
138
185
|
|
|
139
|
-
func SplitLongWord(word string, width int) []string {
|
|
140
|
-
runes := []rune(word)
|
|
141
|
-
lines := []string{}
|
|
142
|
-
for len(runes) > width {
|
|
143
|
-
lines = append(lines, string(runes[:width]))
|
|
144
|
-
runes = runes[width:]
|
|
145
|
-
}
|
|
146
|
-
if len(runes) > 0 {
|
|
147
|
-
lines = append(lines, string(runes))
|
|
148
|
-
}
|
|
149
|
-
return lines
|
|
150
|
-
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package board
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"regexp"
|
|
4
5
|
"strings"
|
|
5
6
|
"testing"
|
|
6
7
|
|
|
@@ -8,6 +9,12 @@ import (
|
|
|
8
9
|
"github.com/opencode/savepoint/internal/data"
|
|
9
10
|
)
|
|
10
11
|
|
|
12
|
+
var ansiPattern = regexp.MustCompile(`\x1b\[[0-9;?]*[ -/]*[@-~]`)
|
|
13
|
+
|
|
14
|
+
func plainTerminal(s string) string {
|
|
15
|
+
return ansiPattern.ReplaceAllString(s, "")
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
func sampleTask() data.Task {
|
|
12
19
|
return data.Task{
|
|
13
20
|
ID: "E04/T001",
|
|
@@ -20,49 +27,49 @@ func sampleTask() data.Task {
|
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
func TestRenderDetail_containsID(t *testing.T) {
|
|
23
|
-
got := RenderDetail(sampleTask(), 60,
|
|
30
|
+
got := RenderDetail(sampleTask(), 60, nil, 0, 0)
|
|
24
31
|
if !strings.Contains(got, "E04/T001") {
|
|
25
32
|
t.Error("RenderDetail missing task ID")
|
|
26
33
|
}
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
func TestRenderDetail_containsTitle(t *testing.T) {
|
|
30
|
-
got := RenderDetail(sampleTask(), 60,
|
|
37
|
+
got := RenderDetail(sampleTask(), 60, nil, 0, 0)
|
|
31
38
|
if !strings.Contains(got, "My Task") {
|
|
32
39
|
t.Error("RenderDetail missing task title")
|
|
33
40
|
}
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
func TestRenderDetail_containsEpic(t *testing.T) {
|
|
37
|
-
got := RenderDetail(sampleTask(), 60,
|
|
44
|
+
got := RenderDetail(sampleTask(), 60, nil, 0, 0)
|
|
38
45
|
if !strings.Contains(got, "E04-board-components") {
|
|
39
46
|
t.Error("RenderDetail missing epic")
|
|
40
47
|
}
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
func TestRenderDetail_containsRelease(t *testing.T) {
|
|
44
|
-
got := RenderDetail(sampleTask(), 60,
|
|
51
|
+
got := RenderDetail(sampleTask(), 60, nil, 0, 0)
|
|
45
52
|
if !strings.Contains(got, "v1") {
|
|
46
53
|
t.Error("RenderDetail missing release")
|
|
47
54
|
}
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
func TestRenderDetail_containsStatus(t *testing.T) {
|
|
51
|
-
got := RenderDetail(sampleTask(), 60,
|
|
58
|
+
got := RenderDetail(sampleTask(), 60, nil, 0, 0)
|
|
52
59
|
if !strings.Contains(got, "in_progress") {
|
|
53
60
|
t.Error("RenderDetail missing status")
|
|
54
61
|
}
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
func TestRenderDetail_containsPhase(t *testing.T) {
|
|
58
|
-
got := RenderDetail(sampleTask(), 60,
|
|
65
|
+
got := RenderDetail(sampleTask(), 60, nil, 0, 0)
|
|
59
66
|
if !strings.Contains(got, "build") {
|
|
60
67
|
t.Error("RenderDetail missing phase")
|
|
61
68
|
}
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
func TestRenderDetail_containsEscHint(t *testing.T) {
|
|
65
|
-
got := RenderDetail(sampleTask(), 60,
|
|
72
|
+
got := RenderDetail(sampleTask(), 60, nil, 0, 0)
|
|
66
73
|
if !strings.Contains(got, "esc") {
|
|
67
74
|
t.Error("RenderDetail missing esc:close hint")
|
|
68
75
|
}
|
|
@@ -71,35 +78,23 @@ func TestRenderDetail_containsEscHint(t *testing.T) {
|
|
|
71
78
|
func TestRenderDetail_containsDescription(t *testing.T) {
|
|
72
79
|
tk := sampleTask()
|
|
73
80
|
tk.Description = "some description text"
|
|
74
|
-
got := RenderDetail(tk, 60,
|
|
81
|
+
got := RenderDetail(tk, 60, nil, 0, 0)
|
|
75
82
|
if !strings.Contains(got, "some description text") {
|
|
76
83
|
t.Error("RenderDetail missing description text")
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
86
|
|
|
80
87
|
func TestRenderDetail_noDescriptionSectionWhenEmpty(t *testing.T) {
|
|
81
|
-
got := RenderDetail(sampleTask(), 60,
|
|
88
|
+
got := RenderDetail(sampleTask(), 60, nil, 0, 0)
|
|
82
89
|
if strings.Contains(got, "Description:") {
|
|
83
90
|
t.Error("RenderDetail should not show Description section when empty")
|
|
84
91
|
}
|
|
85
92
|
}
|
|
86
93
|
|
|
87
|
-
func TestRenderDetail_containsAcceptanceCriteria(t *testing.T) {
|
|
88
|
-
tk := sampleTask()
|
|
89
|
-
tk.Acceptance = []string{"criterion one", "criterion two"}
|
|
90
|
-
got := RenderDetail(tk, 60, "")
|
|
91
|
-
if !strings.Contains(got, "criterion one") {
|
|
92
|
-
t.Error("RenderDetail missing first acceptance criterion")
|
|
93
|
-
}
|
|
94
|
-
if !strings.Contains(got, "criterion two") {
|
|
95
|
-
t.Error("RenderDetail missing second acceptance criterion")
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
94
|
func TestRenderDetail_containsChecklist(t *testing.T) {
|
|
100
95
|
tk := sampleTask()
|
|
101
96
|
tk.Checklist = []data.CheckItem{{Text: "first implementation item"}, {Text: "second implementation item", Done: true}}
|
|
102
|
-
got := RenderDetail(tk, 60,
|
|
97
|
+
got := RenderDetail(tk, 60, nil, 0, 0)
|
|
103
98
|
if !strings.Contains(got, "Implementation Plan:") {
|
|
104
99
|
t.Error("RenderDetail missing implementation plan heading")
|
|
105
100
|
}
|
|
@@ -111,10 +106,70 @@ func TestRenderDetail_containsChecklist(t *testing.T) {
|
|
|
111
106
|
}
|
|
112
107
|
}
|
|
113
108
|
|
|
109
|
+
func TestRenderDetail_checklistSingleSentenceGetsOneCheckbox(t *testing.T) {
|
|
110
|
+
tk := sampleTask()
|
|
111
|
+
tk.Checklist = []data.CheckItem{{Text: "single sentence task"}}
|
|
112
|
+
|
|
113
|
+
got := plainTerminal(RenderDetail(tk, 60, nil, 0, 0))
|
|
114
|
+
|
|
115
|
+
if count := strings.Count(got, "[ ]"); count != 1 {
|
|
116
|
+
t.Fatalf("RenderDetail checkbox count = %d, want 1\n%s", count, got)
|
|
117
|
+
}
|
|
118
|
+
if strings.Contains(got, "[x]") {
|
|
119
|
+
t.Fatal("RenderDetail should not render checked marker for unchecked item")
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
func TestRenderDetail_checklistMultiSentenceGetsOneCheckboxPerSentence(t *testing.T) {
|
|
124
|
+
tk := sampleTask()
|
|
125
|
+
tk.Checklist = []data.CheckItem{{Text: "First sentence. Second sentence! Third sentence?"}}
|
|
126
|
+
|
|
127
|
+
got := plainTerminal(RenderDetail(tk, 60, nil, 0, 0))
|
|
128
|
+
|
|
129
|
+
if count := strings.Count(got, "[ ]"); count != 3 {
|
|
130
|
+
t.Fatalf("RenderDetail checkbox count = %d, want 3\n%s", count, got)
|
|
131
|
+
}
|
|
132
|
+
for _, want := range []string{"[ ] First sentence.", "[ ] Second sentence!", "[ ] Third sentence?"} {
|
|
133
|
+
if !strings.Contains(got, want) {
|
|
134
|
+
t.Fatalf("RenderDetail missing sentence checkbox line %q\n%s", want, got)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
func TestRenderDetail_checklistHardWrappedSentenceDoesNotDuplicateCheckbox(t *testing.T) {
|
|
140
|
+
tk := sampleTask()
|
|
141
|
+
tk.Checklist = []data.CheckItem{{
|
|
142
|
+
Text: "This sentence is intentionally long enough to wrap inside a narrow detail overlay while remaining one semantic sentence.",
|
|
143
|
+
}}
|
|
144
|
+
|
|
145
|
+
got := plainTerminal(RenderDetail(tk, 34, nil, 0, 0))
|
|
146
|
+
|
|
147
|
+
if count := strings.Count(got, "[ ]"); count != 1 {
|
|
148
|
+
t.Fatalf("RenderDetail checkbox count = %d, want 1\n%s", count, got)
|
|
149
|
+
}
|
|
150
|
+
if !strings.Contains(got, " intentionally long enough") {
|
|
151
|
+
t.Fatalf("RenderDetail continuation line should align under checkbox text\n%s", got)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
func TestRenderDetail_checklistCheckedSentenceUsesCheckedMarker(t *testing.T) {
|
|
156
|
+
tk := sampleTask()
|
|
157
|
+
tk.Checklist = []data.CheckItem{{Text: "already done. still done.", Done: true}}
|
|
158
|
+
|
|
159
|
+
got := plainTerminal(RenderDetail(tk, 60, nil, 0, 0))
|
|
160
|
+
|
|
161
|
+
if count := strings.Count(got, "[x]"); count != 2 {
|
|
162
|
+
t.Fatalf("RenderDetail checked checkbox count = %d, want 2\n%s", count, got)
|
|
163
|
+
}
|
|
164
|
+
if strings.Contains(got, "[ ]") {
|
|
165
|
+
t.Fatal("RenderDetail should not render unchecked marker for checked item")
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
114
169
|
func TestRenderDetail_wrapsLongDescription(t *testing.T) {
|
|
115
170
|
tk := sampleTask()
|
|
116
171
|
tk.Description = "alpha beta gamma delta epsilon zeta eta theta iota kappa lambda"
|
|
117
|
-
got := RenderDetail(tk, 30,
|
|
172
|
+
got := RenderDetail(tk, 30, nil, 0, 0)
|
|
118
173
|
if strings.Contains(got, tk.Description) {
|
|
119
174
|
t.Error("RenderDetail should wrap long description text")
|
|
120
175
|
}
|
|
@@ -124,7 +179,7 @@ func TestRenderDetail_wrapsLongDescription(t *testing.T) {
|
|
|
124
179
|
}
|
|
125
180
|
|
|
126
181
|
func TestRenderDetail_noAcceptanceSectionWhenEmpty(t *testing.T) {
|
|
127
|
-
got := RenderDetail(sampleTask(), 60,
|
|
182
|
+
got := RenderDetail(sampleTask(), 60, nil, 0, 0)
|
|
128
183
|
if strings.Contains(got, "Acceptance Criteria:") {
|
|
129
184
|
t.Error("RenderDetail should not show Acceptance section when empty")
|
|
130
185
|
}
|
|
@@ -216,7 +271,8 @@ func TestView_detailOverlayRendered(t *testing.T) {
|
|
|
216
271
|
|
|
217
272
|
func TestRenderDetail_routerPriorityLabel(t *testing.T) {
|
|
218
273
|
task := sampleTask()
|
|
219
|
-
|
|
274
|
+
router := &data.RouterState{Release: task.Release, Epic: task.Epic, Task: task.ID}
|
|
275
|
+
got := RenderDetail(task, 60, router, 0, 0)
|
|
220
276
|
if !strings.Contains(got, "(router priority)") {
|
|
221
277
|
t.Error("RenderDetail missing router priority label for matching task")
|
|
222
278
|
}
|
|
@@ -224,12 +280,47 @@ func TestRenderDetail_routerPriorityLabel(t *testing.T) {
|
|
|
224
280
|
|
|
225
281
|
func TestRenderDetail_noRouterPriorityLabelWhenNoMatch(t *testing.T) {
|
|
226
282
|
task := sampleTask()
|
|
227
|
-
|
|
283
|
+
router := &data.RouterState{Release: task.Release, Epic: task.Epic, Task: "other-id"}
|
|
284
|
+
got := RenderDetail(task, 60, router, 0, 0)
|
|
228
285
|
if strings.Contains(got, "(router priority)") {
|
|
229
286
|
t.Error("RenderDetail should not show router priority label for non-matching task")
|
|
230
287
|
}
|
|
231
288
|
}
|
|
232
289
|
|
|
290
|
+
func TestRenderDetail_viewportShowsScrollIndicators(t *testing.T) {
|
|
291
|
+
task := sampleTask()
|
|
292
|
+
task.Description = "alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu nu xi omicron"
|
|
293
|
+
|
|
294
|
+
got := RenderDetail(task, 32, nil, 8, 2)
|
|
295
|
+
|
|
296
|
+
if !strings.Contains(got, "↑ 2 above") {
|
|
297
|
+
t.Error("RenderDetail missing above indicator")
|
|
298
|
+
}
|
|
299
|
+
if !strings.Contains(got, "↓") || !strings.Contains(got, "more") {
|
|
300
|
+
t.Error("RenderDetail missing more indicator")
|
|
301
|
+
}
|
|
302
|
+
if strings.Contains(got, "ID:") {
|
|
303
|
+
t.Error("RenderDetail should not render body lines above viewport")
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
func TestUpdate_detailOverlayScrollsWithJK(t *testing.T) {
|
|
308
|
+
m := NewModel([]data.Task{sampleTask()}, "v1", "E04-board-components")
|
|
309
|
+
m.Overlay = OverlayDetail
|
|
310
|
+
|
|
311
|
+
got, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("j")})
|
|
312
|
+
updated := requireModel(t, got)
|
|
313
|
+
if updated.DetailOffset != 1 {
|
|
314
|
+
t.Errorf("DetailOffset after j = %d, want 1", updated.DetailOffset)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
got, _ = updated.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("k")})
|
|
318
|
+
updated = requireModel(t, got)
|
|
319
|
+
if updated.DetailOffset != 0 {
|
|
320
|
+
t.Errorf("DetailOffset after k = %d, want 0", updated.DetailOffset)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
233
324
|
func TestOverlayWidth_clampMax(t *testing.T) {
|
|
234
325
|
if got := overlayWidth(120); got != 80 {
|
|
235
326
|
t.Errorf("overlayWidth(120) = %d, want 80", got)
|