hadara 0.1.0-rc.0

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 (121) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/dist/agent/evidence.js +50 -0
  4. package/dist/agent/loop.js +124 -0
  5. package/dist/cli/args.js +70 -0
  6. package/dist/cli/dashboard.js +185 -0
  7. package/dist/cli/debt.js +41 -0
  8. package/dist/cli/doctor.js +68 -0
  9. package/dist/cli/errors.js +58 -0
  10. package/dist/cli/evidence-json.js +75 -0
  11. package/dist/cli/evidence.js +80 -0
  12. package/dist/cli/handoff.js +16 -0
  13. package/dist/cli/harness.js +57 -0
  14. package/dist/cli/hermes-json.js +31 -0
  15. package/dist/cli/hermes.js +28 -0
  16. package/dist/cli/init.js +142 -0
  17. package/dist/cli/install.js +34 -0
  18. package/dist/cli/main.js +216 -0
  19. package/dist/cli/mcp.js +15 -0
  20. package/dist/cli/package-smoke.js +37 -0
  21. package/dist/cli/policy-json.js +22 -0
  22. package/dist/cli/policy.js +43 -0
  23. package/dist/cli/release-artifact.js +47 -0
  24. package/dist/cli/release-dry-run.js +24 -0
  25. package/dist/cli/release-gate.js +28 -0
  26. package/dist/cli/release-publish.js +41 -0
  27. package/dist/cli/run-scaffold.js +68 -0
  28. package/dist/cli/run-state.js +41 -0
  29. package/dist/cli/run.js +191 -0
  30. package/dist/cli/smoke.js +58 -0
  31. package/dist/cli/status-json.js +6 -0
  32. package/dist/cli/status.js +26 -0
  33. package/dist/cli/task-json.js +8 -0
  34. package/dist/cli/task.js +64 -0
  35. package/dist/cli/tools.js +25 -0
  36. package/dist/cli/tui.js +72 -0
  37. package/dist/cli/write-preflight.js +27 -0
  38. package/dist/core/audit.js +41 -0
  39. package/dist/core/events.js +63 -0
  40. package/dist/core/fs.js +44 -0
  41. package/dist/core/paths.js +59 -0
  42. package/dist/core/redaction.js +178 -0
  43. package/dist/core/schema.js +253 -0
  44. package/dist/core/workspace.js +47 -0
  45. package/dist/evidence/evidence.js +170 -0
  46. package/dist/evidence/private-manifest.js +101 -0
  47. package/dist/handoff/handoff.js +49 -0
  48. package/dist/harness/replay.js +200 -0
  49. package/dist/harness/validate.js +465 -0
  50. package/dist/hermes/context-export.js +104 -0
  51. package/dist/index.js +29 -0
  52. package/dist/mcp/server.js +104 -0
  53. package/dist/mcp/tool-dispatch.js +159 -0
  54. package/dist/mcp/tool-registry.js +150 -0
  55. package/dist/mcp/tool-schemas.js +18 -0
  56. package/dist/policy/command-risk.js +39 -0
  57. package/dist/policy/permission-matrix.js +42 -0
  58. package/dist/policy/policy.js +20 -0
  59. package/dist/policy/preflight.js +47 -0
  60. package/dist/policy/presets.js +24 -0
  61. package/dist/policy/tokenizer.js +53 -0
  62. package/dist/providers/fallback-executor.js +46 -0
  63. package/dist/providers/mock-provider.js +49 -0
  64. package/dist/providers/provider-contract.js +2 -0
  65. package/dist/providers/provider-preparation.js +220 -0
  66. package/dist/providers/scripted-provider.js +69 -0
  67. package/dist/schemas/active-run-projection.schema.json +73 -0
  68. package/dist/schemas/active-run-resume.schema.json +68 -0
  69. package/dist/schemas/clean-checkout-smoke.schema.json +126 -0
  70. package/dist/schemas/context-export.schema.json +35 -0
  71. package/dist/schemas/event.schema.json +17 -0
  72. package/dist/schemas/evidence-list.schema.json +49 -0
  73. package/dist/schemas/feature-smoke.schema.json +67 -0
  74. package/dist/schemas/install-plan.schema.json +93 -0
  75. package/dist/schemas/package-smoke.schema.json +130 -0
  76. package/dist/schemas/private-evidence.schema.json +48 -0
  77. package/dist/schemas/provider-call.schema.json +42 -0
  78. package/dist/schemas/provider-config.schema.json +43 -0
  79. package/dist/schemas/release-artifact-manifest.schema.json +55 -0
  80. package/dist/schemas/release-artifact.schema.json +140 -0
  81. package/dist/schemas/release-dry-run.schema.json +141 -0
  82. package/dist/schemas/release-gate.schema.json +42 -0
  83. package/dist/schemas/release-publish.schema.json +114 -0
  84. package/dist/schemas/schema-index.json +145 -0
  85. package/dist/schemas/smoke-evidence-summary.schema.json +88 -0
  86. package/dist/schemas/tools-list.schema.json +78 -0
  87. package/dist/schemas/write-preflight.schema.json +47 -0
  88. package/dist/services/active-run-state.js +215 -0
  89. package/dist/services/capability-registry.js +540 -0
  90. package/dist/services/clean-checkout-smoke.js +393 -0
  91. package/dist/services/evidence-list.js +136 -0
  92. package/dist/services/feature-smoke.js +155 -0
  93. package/dist/services/harness-service.js +7 -0
  94. package/dist/services/install-plan.js +233 -0
  95. package/dist/services/operational-debt.js +767 -0
  96. package/dist/services/operations-status-service.js +195 -0
  97. package/dist/services/package-smoke.js +676 -0
  98. package/dist/services/policy-service.js +25 -0
  99. package/dist/services/project-read-model.js +101 -0
  100. package/dist/services/release-artifact-evidence.js +77 -0
  101. package/dist/services/release-artifact.js +351 -0
  102. package/dist/services/release-dry-run.js +253 -0
  103. package/dist/services/release-evidence.js +138 -0
  104. package/dist/services/release-publish.js +163 -0
  105. package/dist/services/smoke-evidence.js +104 -0
  106. package/dist/services/task-read-model.js +125 -0
  107. package/dist/services/tools-list.js +26 -0
  108. package/dist/services/write-preflight.js +240 -0
  109. package/dist/task/task-capsule.js +121 -0
  110. package/dist/tools/fake-shell.js +56 -0
  111. package/dist/tui/cache.js +341 -0
  112. package/dist/tui/constants.js +44 -0
  113. package/dist/tui/layout.js +140 -0
  114. package/dist/tui/markdown.js +238 -0
  115. package/dist/tui/read-model-worker.js +24 -0
  116. package/dist/tui/read-model.js +502 -0
  117. package/dist/tui/snapshot.js +434 -0
  118. package/dist/tui/state.js +229 -0
  119. package/dist/tui/terminal.js +475 -0
  120. package/dist/tui/theme.js +86 -0
  121. package/package.json +16 -0
@@ -0,0 +1,434 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderTuiSnapshot = renderTuiSnapshot;
4
+ exports.getTuiDocumentScrollBounds = getTuiDocumentScrollBounds;
5
+ const constants_1 = require("./constants");
6
+ const layout_1 = require("./layout");
7
+ const markdown_1 = require("./markdown");
8
+ const theme_1 = require("./theme");
9
+ const DEFAULT_WIDTH = 100;
10
+ const DEFAULT_HEIGHT = 32;
11
+ const MOCKUP_MIN_WIDTH = 78;
12
+ const MOCKUP_MIN_HEIGHT = 24;
13
+ const COMPACT_MIN_WIDTH = 40;
14
+ const COMPACT_MIN_HEIGHT = 10;
15
+ function renderTuiSnapshot(model, options = {}) {
16
+ const terminal = resolveTerminalSize(options);
17
+ const panel = options.panel ?? 'overview';
18
+ const theme = (0, theme_1.normalizeTuiThemeName)(options.theme, 'none');
19
+ const hitboxes = [];
20
+ const rawLines = renderFrame(model, panel, (0, constants_1.resolveTuiDocumentTab)(options.document), terminal.width, terminal.height, hitboxes, {
21
+ includeGeneratedAt: Boolean(options.includeGeneratedAt),
22
+ theme,
23
+ loading: options.loading === true,
24
+ loadingTick: options.loadingTick ?? 0,
25
+ logLine: options.logLine,
26
+ selectedTaskId: options.selectedTaskId ?? model.selectedTaskId,
27
+ taskSearch: options.taskSearch ?? '',
28
+ taskSearchActive: options.taskSearchActive === true,
29
+ taskListScroll: Math.max(0, Math.floor(options.taskListScroll ?? 0)),
30
+ documentScroll: Math.max(0, Math.floor(options.documentScroll ?? 0))
31
+ });
32
+ const truncated = rawLines.length > terminal.height;
33
+ const lines = rawLines.slice(0, terminal.height).map((line) => (theme === 'none' ? (0, layout_1.fit)(line, terminal.width) : (0, layout_1.fitAnsi)(line, terminal.width)));
34
+ while (lines.length < terminal.height)
35
+ lines.push(''.padEnd(terminal.width));
36
+ return {
37
+ schemaVersion: 'hadara.tui.snapshot.internal.v1',
38
+ command: 'tui.snapshot',
39
+ panel,
40
+ terminal: {
41
+ width: terminal.width,
42
+ height: terminal.height,
43
+ color: (0, theme_1.tuiColorEnabled)(theme),
44
+ theme
45
+ },
46
+ text: lines.join('\n'),
47
+ lines,
48
+ hitboxes,
49
+ truncated
50
+ };
51
+ }
52
+ function resolveTerminalSize(options) {
53
+ const policy = options.widthPolicy ?? 'mockup';
54
+ const minWidth = policy === 'compact' ? COMPACT_MIN_WIDTH : MOCKUP_MIN_WIDTH;
55
+ const minHeight = policy === 'compact' ? COMPACT_MIN_HEIGHT : MOCKUP_MIN_HEIGHT;
56
+ return {
57
+ width: Math.max(minWidth, Math.floor(options.width ?? DEFAULT_WIDTH)),
58
+ height: Math.max(minHeight, Math.floor(options.height ?? DEFAULT_HEIGHT))
59
+ };
60
+ }
61
+ function getTuiDocumentScrollBounds(model, options = {}) {
62
+ const terminal = resolveTerminalSize(options);
63
+ const document = (0, constants_1.resolveTuiDocumentTab)(options.document);
64
+ const availableRows = Math.max(1, terminal.height - 8);
65
+ const panelWidth = terminal.width >= 104 ? terminal.width - 25 : terminal.width - 2;
66
+ const docRows = (0, constants_1.tuiDetailDocumentRowsForAvailableRows)(availableRows);
67
+ const docText = model.selectedTask?.detail.files?.[document.file] ?? '';
68
+ const renderedDoc = docText ? (0, markdown_1.renderMarkdownDocument)(docText, Math.max(12, panelWidth - 4)) : [`${document.file} unavailable.`];
69
+ return {
70
+ maxScroll: Math.max(0, renderedDoc.length - docRows),
71
+ renderedRows: renderedDoc.length,
72
+ visibleRows: docRows
73
+ };
74
+ }
75
+ function renderFrame(model, panel, document, width, height, hitboxes, options) {
76
+ const raw = renderHeader(model, document, width, options);
77
+ const availableRows = Math.max(1, height - raw.length - 3);
78
+ if (width >= 104) {
79
+ const navWidth = 22;
80
+ const mainWidth = width - navWidth - 3;
81
+ const contentStartY = raw.length + 1;
82
+ const mainStartX = navWidth + 4;
83
+ const main = renderPanel(model, panel, document, mainWidth, availableRows, options, hitboxes, mainStartX, contentStartY);
84
+ const nav = renderNav(panel, navWidth, options.theme, hitboxes, 1, contentStartY);
85
+ const rows = Math.min(Math.max(nav.length, main.length), availableRows);
86
+ for (let index = 0; index < rows; index += 1) {
87
+ raw.push(`${(0, layout_1.padAnsi)(nav[index] ?? '', navWidth)} ${(0, theme_1.tuiFg)(options.theme, 'border', '│')} ${(0, layout_1.padAnsi)(main[index] ?? '', mainWidth)}`);
88
+ }
89
+ }
90
+ else {
91
+ const tabY = raw.length + 1;
92
+ raw.push(renderTabBar(panel, width, options.theme, hitboxes, tabY));
93
+ raw.push(colorDivider(width, options.theme));
94
+ const panelStartY = raw.length + 1;
95
+ raw.push(...renderPanel(model, panel, document, width - 2, availableRows, options, hitboxes, 1, panelStartY).slice(0, availableRows));
96
+ }
97
+ raw.push(colorDivider(width, options.theme));
98
+ raw.push(renderStatusBar(width, options));
99
+ return raw;
100
+ }
101
+ function renderHeader(model, document, width, options) {
102
+ const selected = model.selectedTask;
103
+ const projectLine = options.includeGeneratedAt
104
+ ? `branch ${model.overview.branch} mode local generated ${model.generatedAt}`
105
+ : `branch ${model.overview.branch} mode local`;
106
+ const title = `${(0, theme_1.tuiFg)(options.theme, 'gold2', 'HADARA')} ${(0, theme_1.tuiFg)(options.theme, 'text', 'Work Console')} ${(0, theme_1.tuiFg)(options.theme, 'muted', '·')} ${colorBadge(String(model.overview.health).toUpperCase(), statusThemeRole(model.overview.health), options.theme)} ${colorBadge('READ ONLY', 'pass', options.theme)}`;
107
+ return [
108
+ colorDivider(width, options.theme),
109
+ (0, layout_1.fitAnsi)(title, width),
110
+ colorTextLine(projectLine, width, options.theme, 'muted'),
111
+ (0, layout_1.fitAnsi)(`${(0, theme_1.tuiFg)(options.theme, 'muted', options.loading ? 'loading' : 'task')} ${(0, theme_1.tuiFg)(options.theme, 'gold2', model.selectedTaskId ?? '-')} ${(0, theme_1.tuiFg)(options.theme, 'text2', selected?.summary.title ?? 'no task')} ${(0, theme_1.tuiFg)(options.theme, 'muted', 'doc')} ${(0, theme_1.tuiFg)(options.theme, 'teal2', document.file)}`, width),
112
+ colorDivider(width, options.theme)
113
+ ];
114
+ }
115
+ function renderNav(activePanel, width, theme, hitboxes, startX, startY) {
116
+ return [
117
+ (0, theme_1.tuiFg)(theme, 'muted', ' WORK'),
118
+ ...constants_1.TUI_PANEL_IDS.map((panel, index) => {
119
+ const marker = panel === activePanel ? '>' : ' ';
120
+ const line = (0, layout_1.fit)(`${marker} ${index + 1} ${constants_1.TUI_PANEL_LABELS[panel]}`, width);
121
+ addHitbox(hitboxes, startX, startY + index + 1, width, 1, 'panel', panel);
122
+ return panel === activePanel ? (0, theme_1.tuiBg)(theme, 'panel2', (0, theme_1.tuiFg)(theme, 'gold2', line)) : (0, theme_1.tuiFg)(theme, 'text2', line);
123
+ })
124
+ ];
125
+ }
126
+ function renderTabBar(activePanel, width, theme, hitboxes, y) {
127
+ let cursor = 1;
128
+ const text = constants_1.TUI_PANEL_IDS.map((panel, index) => {
129
+ const plain = panel === activePanel ? `[${index + 1} ${constants_1.TUI_PANEL_LABELS[panel]}]` : ` ${index + 1} ${constants_1.TUI_PANEL_LABELS[panel]} `;
130
+ addHitbox(hitboxes, cursor, y, (0, layout_1.visibleWidth)(plain), 1, 'panel', panel);
131
+ cursor += (0, layout_1.visibleWidth)(plain) + 1;
132
+ return panel === activePanel ? (0, theme_1.tuiBg)(theme, 'panel2', (0, theme_1.tuiFg)(theme, 'gold2', plain)) : (0, theme_1.tuiFg)(theme, 'muted', plain);
133
+ }).join(' ');
134
+ return (0, layout_1.fitAnsi)(text, width);
135
+ }
136
+ function renderPanel(model, panel, document, width, availableRows, options, hitboxes, startX, startY) {
137
+ if (options.loading)
138
+ return renderLoadingPanel(constants_1.TUI_PANEL_LABELS[panel], width, options);
139
+ if (panel === 'tasks')
140
+ return renderTasks(model, width, availableRows, options, hitboxes, startX, startY);
141
+ if (panel === 'detail')
142
+ return renderDetail(model, document, width, availableRows, options, hitboxes, startX, startY);
143
+ if (panel === 'help')
144
+ return renderHelp(width, options.theme);
145
+ return renderOverview(model, width, options.theme);
146
+ }
147
+ function renderOverview(model, width, theme) {
148
+ const current = model.overview.currentWork;
149
+ const previous = model.overview.previousWork;
150
+ const columnGap = 2;
151
+ const leftWidth = width >= 96 ? Math.max(20, Math.floor((width - columnGap) * 0.52)) : width;
152
+ const rightWidth = width >= 96 ? Math.max(20, width - columnGap - leftWidth) : width;
153
+ const currentCard = colorCard('Current Work', workSummaryLines(model, current, model.overview.currentDetail, 'LIVE', theme, Math.max(8, leftWidth - 4), true), leftWidth, theme, 'teal');
154
+ const previousCard = colorCard('Previous Work', workSummaryLines(model, previous, model.overview.previousDetail, previous ? 'FILE' : 'ROUTE', theme, Math.max(8, rightWidth - 4), false), rightWidth, theme, 'violet');
155
+ const top = width >= 96 ? (0, layout_1.columns)(currentCard, previousCard, width) : [...currentCard, '', ...previousCard];
156
+ const signals = [
157
+ `health ${model.overview.health} tasks done ${model.status.tasks.counts.done} partial ${model.status.tasks.counts.partial} unknown ${model.status.tasks.counts.unknown}`,
158
+ `validation ${(0, layout_1.trimFit)(model.status.validation.latestFullCheck ?? '-', Math.max(12, width - 18))}`
159
+ ];
160
+ return [...top, '', ...colorCard('Resume Signals', signals, width, theme, 'gold'), ...nextRecommendedCard(model, width, theme)];
161
+ }
162
+ function renderTasks(model, width, availableRows, options, hitboxes, startX, startY) {
163
+ const theme = options.theme;
164
+ const query = options.taskSearch.trim().toLowerCase();
165
+ const visible = query
166
+ ? model.tasks.tasks.filter((task) => [task.id, task.title, task.status, task.capsule].some((value) => String(value ?? '').toLowerCase().includes(query)))
167
+ : model.tasks.tasks;
168
+ const rowsAll = [...visible].reverse();
169
+ if (!rowsAll.length) {
170
+ return colorCard('Tasks Empty', [`${colorBadge('ROUTE', 'muted', theme)} no tasks match ${query ? `"${options.taskSearch}"` : 'the current project'}`], width, theme, 'warn');
171
+ }
172
+ const visibleRows = (0, constants_1.tuiTaskVisibleRowsForAvailableRows)(availableRows);
173
+ const selectedIndex = options.selectedTaskId ? rowsAll.findIndex((task) => task.id === options.selectedTaskId) : -1;
174
+ const windowStart = normalizeTaskWindow(options.taskListScroll, selectedIndex, rowsAll.length, visibleRows);
175
+ const rows = rowsAll.slice(windowStart, windowStart + visibleRows).map((task, localIndex) => {
176
+ const marker = options.selectedTaskId === task.id ? '>' : ' ';
177
+ const titleWidth = Math.max(10, width - 44);
178
+ const markerText = marker === '>' ? (0, theme_1.tuiFg)(theme, 'gold2', marker) : (0, theme_1.tuiFg)(theme, 'dim', marker);
179
+ const line = `${markerText} ${colorBadge(task.status, statusThemeRole(task.status), theme)} ${(0, theme_1.tuiFg)(theme, 'gold2', (0, layout_1.pad)(task.id, 8))} ${(0, layout_1.fit)(task.title, titleWidth)} ${(0, theme_1.tuiFg)(theme, 'muted', (0, layout_1.trimFit)(task.capsule, 22))}`;
180
+ addHitbox(hitboxes, startX, startY + localIndex + 1, width, 1, 'task', task.id);
181
+ return marker === '>' ? (0, theme_1.tuiBg)(theme, 'panel2', line) : line;
182
+ });
183
+ const searchHint = options.taskSearchActive
184
+ ? `search: ${options.taskSearch}_`
185
+ : query
186
+ ? `search: ${options.taskSearch}`
187
+ : '/ search id/title/status';
188
+ return [
189
+ colorTextLine('Status ID Title Capsule', width, theme, 'muted'),
190
+ ...rows,
191
+ '',
192
+ (0, layout_1.fit)(`${searchHint} · Showing ${windowStart + 1}-${windowStart + rows.length} of ${rowsAll.length}/${model.tasks.count}. Enter/click opens Detail.`, width)
193
+ ];
194
+ }
195
+ function renderDetail(model, document, width, availableRows, options, hitboxes, startX, startY) {
196
+ const theme = options.theme;
197
+ if (!model.selectedTask) {
198
+ return colorCard('Detail Empty', [`${colorBadge('ROUTE', 'muted', theme)} no selected task detail is available`], width, theme, 'warn');
199
+ }
200
+ const task = model.selectedTask.summary;
201
+ const docText = model.selectedTask.detail.files?.[document.file] ?? '';
202
+ const docAvailable = Boolean(docText);
203
+ let tabCursor = startX + 2;
204
+ const tabs = constants_1.TUI_DOCUMENT_TABS.map((tab) => {
205
+ const plain = tab.file === document.file ? `[${tab.key.toUpperCase()} ${tab.shortLabel}]` : ` ${tab.key} ${tab.shortLabel} `;
206
+ addHitbox(hitboxes, tabCursor, startY + 4, (0, layout_1.visibleWidth)(plain), 1, 'document', tab.file);
207
+ tabCursor += (0, layout_1.visibleWidth)(plain) + 1;
208
+ return tab.file === document.file ? (0, theme_1.tuiFg)(theme, 'gold2', plain) : (0, theme_1.tuiFg)(theme, 'muted', plain);
209
+ }).join(' ');
210
+ const meta = [
211
+ `${colorBadge('LIVE', 'pass', theme)} ${(0, theme_1.tuiFg)(theme, 'gold2', task.id)} ${task.title}`,
212
+ `${(0, theme_1.tuiFg)(theme, 'muted', 'status')} ${(0, theme_1.tuiFg)(theme, statusThemeRole(task.status), task.status)} ${(0, theme_1.tuiFg)(theme, 'muted', 'capsule')} ${(0, layout_1.trimFit)(task.capsule || '-', Math.max(12, width - 34))}`,
213
+ `${(0, theme_1.tuiFg)(theme, 'muted', 'detail')} ${model.selectedTask.detail.schemaVersion} ${(0, theme_1.tuiFg)(theme, 'muted', 'document')} ${colorBadge(docAvailable ? 'LIVE' : 'PLANNED', docAvailable ? 'pass' : 'warn', theme)} ${document.file}`,
214
+ (0, layout_1.fitAnsi)(tabs, Math.max(12, width - 4))
215
+ ];
216
+ const docRows = (0, constants_1.tuiDetailDocumentRowsForAvailableRows)(availableRows);
217
+ const renderedDoc = docAvailable ? (0, markdown_1.renderMarkdownDocument)(docText, Math.max(12, width - 4)) : [`${document.file} unavailable.`];
218
+ const maxScroll = Math.max(0, renderedDoc.length - docRows);
219
+ const scroll = Math.min(Math.max(0, options.documentScroll), maxScroll);
220
+ const lines = colorizeDetailDocument(renderedDoc.slice(scroll, scroll + docRows), theme);
221
+ while (lines.length < docRows)
222
+ lines.push('');
223
+ const title = maxScroll > 0 ? `Document Viewer ${document.file} ${scroll + 1}-${Math.min(renderedDoc.length, scroll + docRows)}/${renderedDoc.length}` : `Document Viewer ${document.file}`;
224
+ return [...colorCard('Task Detail', meta, width, theme, 'gold'), '', ...colorCard(title, lines, width, theme, docAvailable ? 'teal' : 'warn')];
225
+ }
226
+ function renderHelp(width, theme) {
227
+ return colorCard('Controls', [
228
+ '1-4 switch panels',
229
+ 'Up/Down select task or scroll document',
230
+ '/ search tasks by id, title, or status',
231
+ 'Esc clear task search',
232
+ 'Enter open detail',
233
+ 't/p/d/a/e/h/f/k/s switch Task Detail document',
234
+ 'r refresh project state',
235
+ '? help',
236
+ 'q quit',
237
+ '',
238
+ 'Boundary: read-only snapshot; no writes, shell execution, provider calls, or MCP calls.'
239
+ ], width, theme, 'gold');
240
+ }
241
+ function renderStatusBar(width, options) {
242
+ const key = (text) => (options.theme === 'none' ? `[${text}]` : (0, theme_1.tuiSwatch)(options.theme, text === '↵' ? 'teal' : 'gold', 'black', ` ${text} `));
243
+ const left = [
244
+ `${key('1-4')} panels`,
245
+ `${key('↑↓')} select`,
246
+ `${key('↵')} detail`,
247
+ `${key('/')} search`,
248
+ `${key('r')} refresh`,
249
+ `${key('?')} help`,
250
+ `${key('q')} quit`
251
+ ].join(options.theme === 'none' ? ' · ' : (0, theme_1.tuiFg)(options.theme, 'dim', ' · '));
252
+ const log = options.logLine ? `${(0, theme_1.tuiFg)(options.theme, 'dim', 'log')} ${(0, theme_1.tuiFg)(options.theme, 'muted', (0, layout_1.trimFitAnsi)(options.logLine, Math.max(8, width - (0, layout_1.visibleWidth)(left) - 8)))}` : '';
253
+ if (!log)
254
+ return (0, layout_1.fitAnsi)(left, width);
255
+ const gap = ' '.repeat(Math.max(1, width - (0, layout_1.visibleWidth)(left) - (0, layout_1.visibleWidth)(log) - 1));
256
+ return (0, layout_1.fitAnsi)(`${left}${gap}${log}`, width);
257
+ }
258
+ function workSummaryLines(model, task, detail, label, theme, width, includeResumeActions) {
259
+ if (!task) {
260
+ return [
261
+ trimLine(`${colorBadge('ROUTE', 'muted', theme)} - No task exposed`, width),
262
+ labelValueLine('status', 'muted', '- · capsule -', width, theme),
263
+ labelValueLine('Goal', 'teal2', 'No summary exposed.', width, theme),
264
+ labelValueLine('Next', 'gold2', 'Select a task or refresh read models.', width, theme),
265
+ labelValueLine('Proof', 'pass', 'No evidence exposed.', width, theme)
266
+ ];
267
+ }
268
+ const docs = detail?.files ?? {};
269
+ const taskText = docs?.['TASK.md'] ?? '';
270
+ const planText = docs?.['PLAN.md'] ?? '';
271
+ const acceptanceText = docs?.['ACCEPTANCE.md'] ?? '';
272
+ const handoffText = docs?.['HANDOFF.md'] ?? '';
273
+ const evidenceText = docs?.['EVIDENCE.md'] ?? '';
274
+ const nextActions = includeResumeActions ? model.activeRun.resume.resumePrompt.nextActions : [];
275
+ const next = firstLine(nextActions, (0, markdown_1.markdownPreview)(handoffText, { headings: ['Next Recommended Step', 'Next'], limit: 2 }), (0, markdown_1.incompleteChecklist)(planText, 2), (0, markdown_1.incompleteChecklist)(acceptanceText, 2), (0, markdown_1.markdownPreview)(planText, { headings: ['Plan'], limit: 2 }), (0, markdown_1.markdownPreview)(acceptanceText, { headings: ['Acceptance'], limit: 2 }), task.status === 'Done' ? 'Completed; use current work as the next operating context.' : '');
276
+ const latestEvidence = task.id === model.selectedTask?.summary.id && model.selectedTask.evidence.records[0]?.summary
277
+ ? [model.selectedTask.evidence.records[0].summary]
278
+ : [];
279
+ const proof = firstLine(latestEvidence, (0, markdown_1.evidenceFromMarkdown)(evidenceText, 2), (0, markdown_1.markdownPreview)(evidenceText, { limit: 2 }), task.status === 'Done' ? 'Done status exposed by task list.' : '');
280
+ return [
281
+ summaryTitleLine(label, task.id, task.title || '-', width, theme),
282
+ summaryStatusLine(task.status || '-', task.capsule || '-', width, theme),
283
+ labelValueLine('Goal', 'teal2', firstLine((0, markdown_1.markdownPreview)(taskText, { headings: ['Goal', 'Current', 'Scope', 'Summary'], limit: 2 }), task.title), width, theme),
284
+ labelValueLine('Next', 'gold2', next, width, theme),
285
+ labelValueLine('Proof', 'pass', proof, width, theme)
286
+ ];
287
+ }
288
+ function summaryTitleLine(label, taskId, title, width, theme) {
289
+ const prefix = `${colorBadge(label, label === 'LIVE' ? 'pass' : 'violet', theme)} ${(0, theme_1.tuiFg)(theme, 'gold2', taskId)} `;
290
+ return `${prefix}${(0, layout_1.trimFit)(title, Math.max(1, width - (0, layout_1.visibleWidth)(prefix)))}`;
291
+ }
292
+ function summaryStatusLine(status, capsule, width, theme) {
293
+ const prefix = `${(0, theme_1.tuiFg)(theme, statusThemeRole(status), status)} ${(0, theme_1.tuiFg)(theme, 'muted', '·')} `;
294
+ return `${prefix}${(0, layout_1.trimFit)(capsule, Math.max(1, width - (0, layout_1.visibleWidth)(prefix)))}`;
295
+ }
296
+ function labelValueLine(label, role, value, width, theme) {
297
+ const prefix = `${(0, theme_1.tuiFg)(theme, role, label)} `;
298
+ return `${prefix}${(0, layout_1.trimFit)(value, Math.max(1, width - (0, layout_1.visibleWidth)(prefix)))}`;
299
+ }
300
+ function trimLine(line, width) {
301
+ return (0, layout_1.trimFitAnsi)(line, width);
302
+ }
303
+ function nextRecommendedCard(model, width, theme) {
304
+ const next = [
305
+ model.status.tasks.nextRecommended,
306
+ ...(model.status.handoff.nextRecommendedStep ?? [])
307
+ ].filter((line) => Boolean(line));
308
+ if (!next.length)
309
+ return [];
310
+ return ['', ...colorCard('Next Recommended', dedupe(next).slice(0, 4), width, theme, 'gold')];
311
+ }
312
+ function dedupe(items) {
313
+ const seen = new Set();
314
+ return items.filter((item) => {
315
+ if (seen.has(item))
316
+ return false;
317
+ seen.add(item);
318
+ return true;
319
+ });
320
+ }
321
+ function normalizeTaskWindow(requestedStart, selectedIndex, rowCount, visibleRows) {
322
+ if (rowCount <= visibleRows)
323
+ return 0;
324
+ let start = Math.min(Math.max(0, requestedStart), Math.max(0, rowCount - visibleRows));
325
+ if (selectedIndex >= 0 && selectedIndex < start)
326
+ start = selectedIndex;
327
+ if (selectedIndex >= 0 && selectedIndex >= start + visibleRows)
328
+ start = selectedIndex - visibleRows + 1;
329
+ return Math.min(Math.max(0, start), Math.max(0, rowCount - visibleRows));
330
+ }
331
+ function renderLoadingPanel(panel, width, options) {
332
+ return colorCard(`${panel} Reading`, [
333
+ ...loaderLines(width, options.theme, options.loadingTick),
334
+ '',
335
+ `${colorBadge('READ', 'pass', options.theme)} project state`,
336
+ `${(0, theme_1.tuiFg)(options.theme, 'muted', 'signal')} selected task and capsule documents requested`
337
+ ], width, options.theme, 'teal');
338
+ }
339
+ function loaderLines(width, theme, tick) {
340
+ const innerWidth = 24;
341
+ const dotCount = 11;
342
+ const position = Math.abs(tick) % dotCount;
343
+ const rail = Array.from({ length: dotCount }, (_, index) => (0, theme_1.tuiFg)(theme, index === position ? 'teal2' : 'muted', index === position ? '●' : '·')).join((0, theme_1.tuiFg)(theme, 'muted', ' '));
344
+ const indent = ' '.repeat(Math.max(0, Math.floor((width - (innerWidth + 2)) / 2)));
345
+ const boxed = (content) => `${indent}${(0, theme_1.tuiFg)(theme, 'dim', '│')}${(0, layout_1.padAnsi)(content, innerWidth)}${(0, theme_1.tuiFg)(theme, 'dim', '│')}`;
346
+ return [
347
+ `${indent}${(0, theme_1.tuiFg)(theme, 'dim', '╭')}${(0, theme_1.tuiFg)(theme, 'dim', '─'.repeat(innerWidth))}${(0, theme_1.tuiFg)(theme, 'dim', '╮')}`,
348
+ boxed(` ${rail}`),
349
+ boxed((0, theme_1.tuiFg)(theme, 'gold', ' reading task capsule')),
350
+ `${indent}${(0, theme_1.tuiFg)(theme, 'dim', '╰')}${(0, theme_1.tuiFg)(theme, 'dim', '─'.repeat(innerWidth))}${(0, theme_1.tuiFg)(theme, 'dim', '╯')}`
351
+ ];
352
+ }
353
+ function colorCard(title, lines, width, theme, accent = 'gold') {
354
+ if (theme === 'none')
355
+ return (0, layout_1.card)(title, lines, width);
356
+ const inner = Math.max(8, width - 4);
357
+ const head = ` ${title} `;
358
+ return [
359
+ `${(0, theme_1.tuiFg)(theme, accent, '╭─')}${(0, theme_1.tuiFg)(theme, accent, (0, layout_1.fit)(head, Math.min((0, layout_1.visibleWidth)(head), inner)))}${(0, theme_1.tuiFg)(theme, accent, '─'.repeat(Math.max(0, inner - (0, layout_1.visibleWidth)(head))))}${(0, theme_1.tuiFg)(theme, accent, '─╮')}`,
360
+ ...lines.map((line) => `${(0, theme_1.tuiFg)(theme, 'border', '│')} ${(0, layout_1.padAnsi)(line, inner)} ${(0, theme_1.tuiFg)(theme, 'border', '│')}`),
361
+ `${(0, theme_1.tuiFg)(theme, 'border', '╰')}${(0, theme_1.tuiFg)(theme, 'border', '─'.repeat(inner + 2))}${(0, theme_1.tuiFg)(theme, 'border', '╯')}`
362
+ ];
363
+ }
364
+ function colorBadge(text, role, theme) {
365
+ if (theme === 'none')
366
+ return (0, layout_1.badge)(text);
367
+ return (0, theme_1.tuiSwatch)(theme, role, role === 'fail' ? 'white' : 'black', ` ${String(text).toUpperCase()} `);
368
+ }
369
+ function colorDivider(width, theme) {
370
+ return (0, theme_1.tuiFg)(theme, 'border', (0, layout_1.divider)(width));
371
+ }
372
+ function colorTextLine(text, width, theme, role) {
373
+ return (0, theme_1.tuiFg)(theme, role, (0, layout_1.fit)(text, width));
374
+ }
375
+ function statusThemeRole(value) {
376
+ const normalized = String(value ?? '').toLowerCase();
377
+ if (['ok', 'done', 'passed', 'true', 'read', 'preview'].includes(normalized))
378
+ return 'pass';
379
+ if (['warning', 'partial', 'draft', 'medium'].includes(normalized))
380
+ return 'warn';
381
+ if (['error', 'failed', 'high', 'disabled', 'blocked'].includes(normalized))
382
+ return 'fail';
383
+ return 'teal';
384
+ }
385
+ function colorizeDetailDocument(lines, theme) {
386
+ if (theme === 'none')
387
+ return lines;
388
+ return lines.map((line, index) => {
389
+ const plain = line.trimEnd();
390
+ if (!plain)
391
+ return line;
392
+ if (/^─{8,}$/.test(plain))
393
+ return (0, theme_1.tuiFg)(theme, 'border', line);
394
+ if (/^[-─┼]{8,}$/.test(plain))
395
+ return (0, theme_1.tuiFg)(theme, 'border', line);
396
+ if (/^─/.test(lines[index + 1]?.trimEnd() ?? ''))
397
+ return (0, theme_1.tuiFg)(theme, index === 0 || !lines.slice(0, index).some((candidate) => candidate.trim()) ? 'gold2' : 'teal2', line);
398
+ if (/^[A-Z0-9][A-Z0-9 _/-]{2,}$/.test(plain) && (0, layout_1.visibleWidth)(plain) <= 48)
399
+ return (0, theme_1.tuiFg)(theme, 'teal2', line);
400
+ if (/^\[(DONE|TODO|LIVE|PLANNED|READY|FILE)\]/i.test(plain)) {
401
+ return line.replace(/^\[([^\]]+)\]/, (_match, label) => colorBadge(label, label.toLowerCase() === 'done' ? 'pass' : 'warn', theme));
402
+ }
403
+ if (/^\[[ xX]\]\s+/.test(plain)) {
404
+ return line.replace(/^\[([ xX])\]\s+/, (_match, mark) => `${colorBadge(mark.trim() ? 'DONE' : 'TODO', mark.trim() ? 'pass' : 'warn', theme)} `);
405
+ }
406
+ if (plain.startsWith('•'))
407
+ return line.replace('•', (0, theme_1.tuiFg)(theme, 'gold', '•'));
408
+ if (/^\d{2}\s+/.test(plain))
409
+ return line.replace(/^(\d{2})/, (_match, number) => (0, theme_1.tuiFg)(theme, 'gold', number));
410
+ if (/^\d+\.\s+/.test(plain))
411
+ return line.replace(/^(\d+)\./, (_match, number) => (0, theme_1.tuiFg)(theme, 'gold', number.padStart(2, '0')));
412
+ return (0, theme_1.tuiFg)(theme, 'text2', line);
413
+ });
414
+ }
415
+ function addHitbox(hitboxes, x, y, width, height, action, payload) {
416
+ if (width <= 0 || height <= 0)
417
+ return;
418
+ hitboxes.push({
419
+ x1: x,
420
+ y1: y,
421
+ x2: x + width - 1,
422
+ y2: y + height - 1,
423
+ action,
424
+ payload
425
+ });
426
+ }
427
+ function firstLine(...values) {
428
+ for (const value of values.flat()) {
429
+ const cleaned = String(value ?? '').trim();
430
+ if (cleaned)
431
+ return cleaned;
432
+ }
433
+ return 'No concise summary exposed.';
434
+ }
@@ -0,0 +1,229 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createTuiInteractionState = createTuiInteractionState;
4
+ exports.reduceTuiInteractionState = reduceTuiInteractionState;
5
+ exports.getTuiTaskRows = getTuiTaskRows;
6
+ exports.tuiStateToReadModelOptions = tuiStateToReadModelOptions;
7
+ exports.tuiStateToSnapshotOptions = tuiStateToSnapshotOptions;
8
+ const constants_1 = require("./constants");
9
+ const DEFAULT_PAGE_SIZE = 8;
10
+ const DEFAULT_TASK_VISIBLE_ROWS = 12;
11
+ function createTuiInteractionState(model, options = {}) {
12
+ const panel = resolvePanel(options.panel, 'overview');
13
+ const document = (0, constants_1.resolveTuiDocumentTab)(options.document).file;
14
+ const taskSearch = options.taskSearch ?? '';
15
+ const rows = filterTaskRows(getTaskRows(model), taskSearch);
16
+ const requestedTaskId = options.selectedTaskId ?? model.selectedTaskId;
17
+ const selectedTaskIndex = resolveTaskIndex(rows, requestedTaskId);
18
+ const selectedTask = selectedTaskIndex >= 0 ? rows[selectedTaskIndex] : null;
19
+ const taskListVisibleRows = normalizeVisibleRows(options.taskListVisibleRows);
20
+ return {
21
+ activePanel: panel,
22
+ selectedTaskId: selectedTask?.id ?? null,
23
+ selectedTaskIndex,
24
+ taskSearch,
25
+ searchActive: Boolean(options.searchActive),
26
+ documentFile: document,
27
+ documentScroll: 0,
28
+ taskListScroll: normalizeTaskOffset(0, selectedTaskIndex, rows.length, taskListVisibleRows),
29
+ refreshRequested: false,
30
+ quitRequested: false,
31
+ detailRefreshRequested: false
32
+ };
33
+ }
34
+ function reduceTuiInteractionState(state, model, key, options = {}) {
35
+ const normalized = normalizeKey(key);
36
+ const taskListVisibleRows = normalizeVisibleRows(options.taskListVisibleRows);
37
+ if (normalized === 'refresh-complete' || normalized === 'refresh-failed')
38
+ return { ...state, refreshRequested: false };
39
+ if (normalized === 'detail-refresh-complete' || normalized === 'detail-refresh-failed')
40
+ return { ...state, detailRefreshRequested: false };
41
+ if (normalized === 'ctrl-c' || normalized === 'q' || normalized === 'ㅂ')
42
+ return { ...state, quitRequested: true };
43
+ if (state.searchActive) {
44
+ if (normalized === 'enter')
45
+ return { ...state, searchActive: false };
46
+ if (normalized === 'escape')
47
+ return reconcileTaskSelection({ ...state, searchActive: false, taskSearch: '', taskListScroll: 0 }, model, taskListVisibleRows);
48
+ if (normalized === 'backspace') {
49
+ return reconcileTaskSelection({ ...state, taskSearch: state.taskSearch.slice(0, -1), taskListScroll: 0 }, model, taskListVisibleRows);
50
+ }
51
+ if (isSearchCharacter(key)) {
52
+ return reconcileTaskSelection({ ...state, taskSearch: `${state.taskSearch}${key}`, taskListScroll: 0 }, model, taskListVisibleRows);
53
+ }
54
+ }
55
+ if (normalized === 'r')
56
+ return { ...state, refreshRequested: true };
57
+ if (normalized === '?')
58
+ return { ...state, activePanel: 'help' };
59
+ if (normalized === 'tab' || normalized === 'right')
60
+ return { ...state, activePanel: nextPanel(state.activePanel, 1), searchActive: false };
61
+ if (normalized === 'shift-tab' || normalized === 'left')
62
+ return { ...state, activePanel: nextPanel(state.activePanel, -1), searchActive: false };
63
+ const panel = panelForKey(normalized);
64
+ if (panel)
65
+ return { ...state, activePanel: panel, searchActive: panel === 'tasks' ? state.searchActive : false };
66
+ if (normalized === '/')
67
+ return reconcileTaskSelection({ ...state, activePanel: 'tasks', searchActive: true, taskSearch: '', taskListScroll: 0 }, model, taskListVisibleRows);
68
+ if (normalized === 'escape')
69
+ return reconcileTaskSelection({ ...state, searchActive: false, taskSearch: '', taskListScroll: 0 }, model, taskListVisibleRows);
70
+ const document = documentForKey(normalized);
71
+ if (document)
72
+ return { ...state, activePanel: 'detail', documentFile: document.file, documentScroll: 0, searchActive: false };
73
+ if (normalized === 'enter' && state.activePanel === 'tasks' && state.selectedTaskId) {
74
+ return {
75
+ ...state,
76
+ activePanel: 'detail',
77
+ searchActive: false,
78
+ documentScroll: 0,
79
+ detailRefreshRequested: model.selectedTaskId !== state.selectedTaskId
80
+ };
81
+ }
82
+ if (state.activePanel === 'tasks' && (normalized === 'up' || normalized === 'down')) {
83
+ return moveTaskSelection(state, model, normalized === 'down' ? 1 : -1, taskListVisibleRows);
84
+ }
85
+ if (state.activePanel === 'tasks') {
86
+ if (normalized === 'pageup')
87
+ return moveTaskSelection(state, model, -taskListVisibleRows, taskListVisibleRows);
88
+ if (normalized === 'pagedown')
89
+ return moveTaskSelection(state, model, taskListVisibleRows, taskListVisibleRows);
90
+ if (normalized === 'home')
91
+ return moveTaskSelectionToEdge(state, model, 'start', taskListVisibleRows);
92
+ if (normalized === 'end')
93
+ return moveTaskSelectionToEdge(state, model, 'end', taskListVisibleRows);
94
+ }
95
+ if (state.activePanel === 'detail') {
96
+ const maxScroll = normalizeDocumentMaxScroll(options.documentMaxScroll);
97
+ if (normalized === 'up')
98
+ return { ...state, documentScroll: Math.max(0, clampDocumentScroll(state.documentScroll, maxScroll) - 1) };
99
+ if (normalized === 'down')
100
+ return { ...state, documentScroll: clampDocumentScroll(state.documentScroll + 1, maxScroll) };
101
+ if (normalized === 'pageup')
102
+ return { ...state, documentScroll: Math.max(0, clampDocumentScroll(state.documentScroll, maxScroll) - DEFAULT_PAGE_SIZE) };
103
+ if (normalized === 'pagedown')
104
+ return { ...state, documentScroll: clampDocumentScroll(state.documentScroll + DEFAULT_PAGE_SIZE, maxScroll) };
105
+ if (normalized === 'home')
106
+ return { ...state, documentScroll: 0 };
107
+ if (normalized === 'end')
108
+ return { ...state, documentScroll: maxScroll ?? Number.MAX_SAFE_INTEGER };
109
+ }
110
+ return state;
111
+ }
112
+ function normalizeDocumentMaxScroll(value) {
113
+ if (value === undefined)
114
+ return null;
115
+ return Math.max(0, Math.floor(value));
116
+ }
117
+ function clampDocumentScroll(value, maxScroll) {
118
+ const next = Math.max(0, Math.floor(value));
119
+ return maxScroll === null ? next : Math.min(next, maxScroll);
120
+ }
121
+ function moveTaskSelectionToEdge(state, model, edge, visibleRows) {
122
+ const rows = getTuiTaskRows(model, state);
123
+ if (!rows.length)
124
+ return { ...state, selectedTaskId: null, selectedTaskIndex: -1, taskListScroll: 0 };
125
+ const nextIndex = edge === 'start' ? 0 : rows.length - 1;
126
+ return {
127
+ ...state,
128
+ selectedTaskId: rows[nextIndex]?.id ?? null,
129
+ selectedTaskIndex: nextIndex,
130
+ taskListScroll: normalizeTaskOffset(state.taskListScroll, nextIndex, rows.length, visibleRows)
131
+ };
132
+ }
133
+ function getTuiTaskRows(model, state) {
134
+ return filterTaskRows(getTaskRows(model), state.taskSearch);
135
+ }
136
+ function tuiStateToReadModelOptions(state) {
137
+ return state.selectedTaskId ? { selectedTaskId: state.selectedTaskId } : {};
138
+ }
139
+ function tuiStateToSnapshotOptions(state, options = {}) {
140
+ return {
141
+ ...options,
142
+ panel: state.activePanel,
143
+ document: state.documentFile,
144
+ selectedTaskId: state.selectedTaskId,
145
+ taskSearch: state.taskSearch,
146
+ taskSearchActive: state.searchActive,
147
+ taskListScroll: state.taskListScroll,
148
+ documentScroll: state.documentScroll
149
+ };
150
+ }
151
+ function getTaskRows(model) {
152
+ return [...model.tasks.tasks].reverse();
153
+ }
154
+ function filterTaskRows(tasks, search) {
155
+ const query = search.trim().toLowerCase();
156
+ if (!query)
157
+ return tasks;
158
+ return tasks.filter((task) => [task.id, task.title, task.status, task.capsule].some((value) => String(value ?? '').toLowerCase().includes(query)));
159
+ }
160
+ function reconcileTaskSelection(state, model, visibleRows) {
161
+ const rows = getTuiTaskRows(model, state);
162
+ const index = resolveTaskIndex(rows, state.selectedTaskId);
163
+ const fallbackIndex = index >= 0 ? index : rows.length ? 0 : -1;
164
+ const selectedTask = fallbackIndex >= 0 ? rows[fallbackIndex] : null;
165
+ return {
166
+ ...state,
167
+ selectedTaskId: selectedTask?.id ?? null,
168
+ selectedTaskIndex: fallbackIndex,
169
+ taskListScroll: normalizeTaskOffset(state.taskListScroll, fallbackIndex, rows.length, visibleRows)
170
+ };
171
+ }
172
+ function moveTaskSelection(state, model, delta, visibleRows) {
173
+ const rows = getTuiTaskRows(model, state);
174
+ if (!rows.length)
175
+ return { ...state, selectedTaskId: null, selectedTaskIndex: -1, taskListScroll: 0 };
176
+ const nextIndex = Math.min(rows.length - 1, Math.max(0, state.selectedTaskIndex + delta));
177
+ return {
178
+ ...state,
179
+ selectedTaskId: rows[nextIndex]?.id ?? null,
180
+ selectedTaskIndex: nextIndex,
181
+ taskListScroll: normalizeTaskOffset(state.taskListScroll, nextIndex, rows.length, visibleRows)
182
+ };
183
+ }
184
+ function resolveTaskIndex(tasks, selectedTaskId) {
185
+ if (selectedTaskId) {
186
+ const selectedIndex = tasks.findIndex((task) => task.id === selectedTaskId);
187
+ if (selectedIndex >= 0)
188
+ return selectedIndex;
189
+ }
190
+ return tasks.length ? 0 : -1;
191
+ }
192
+ function normalizeTaskOffset(requestedStart, selectedIndex, rowCount, visibleRows) {
193
+ if (rowCount <= visibleRows || selectedIndex < 0)
194
+ return 0;
195
+ let start = Math.min(Math.max(0, requestedStart), Math.max(0, rowCount - visibleRows));
196
+ if (selectedIndex < start)
197
+ start = selectedIndex;
198
+ if (selectedIndex >= start + visibleRows)
199
+ start = selectedIndex - visibleRows + 1;
200
+ return Math.min(Math.max(0, start), Math.max(0, rowCount - visibleRows));
201
+ }
202
+ function normalizeVisibleRows(value) {
203
+ return Math.max(1, Math.floor(value ?? DEFAULT_TASK_VISIBLE_ROWS));
204
+ }
205
+ function panelForKey(key) {
206
+ const numeric = Number(key);
207
+ if (Number.isInteger(numeric) && numeric >= 1 && numeric <= constants_1.TUI_PANEL_IDS.length)
208
+ return constants_1.TUI_PANEL_IDS[numeric - 1] ?? null;
209
+ return null;
210
+ }
211
+ function nextPanel(current, delta) {
212
+ const index = constants_1.TUI_PANEL_IDS.findIndex((panel) => panel === current);
213
+ return constants_1.TUI_PANEL_IDS[(index + delta + constants_1.TUI_PANEL_IDS.length) % constants_1.TUI_PANEL_IDS.length] ?? current;
214
+ }
215
+ function documentForKey(key) {
216
+ return constants_1.TUI_DOCUMENT_TABS.find((tab) => tab.key === key || tab.file.toLowerCase() === key || tab.shortLabel.toLowerCase() === key) ?? null;
217
+ }
218
+ function resolvePanel(value, fallback) {
219
+ const normalized = String(value ?? '').toLowerCase();
220
+ return constants_1.TUI_PANEL_IDS.find((panel) => panel === normalized) ?? fallback;
221
+ }
222
+ function normalizeKey(key) {
223
+ if (key.length === 1)
224
+ return key.toLowerCase();
225
+ return key.toLowerCase();
226
+ }
227
+ function isSearchCharacter(key) {
228
+ return typeof key === 'string' && key.length === 1 && key >= ' ' && key !== '\x7f';
229
+ }