@jmoyers/harness 0.1.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.
- package/LICENSE +21 -0
- package/README.md +145 -0
- package/native/ptyd/Cargo.lock +16 -0
- package/native/ptyd/Cargo.toml +7 -0
- package/native/ptyd/src/main.rs +257 -0
- package/package.json +90 -0
- package/scripts/build-ptyd.sh +73 -0
- package/scripts/control-plane-daemon.ts +277 -0
- package/scripts/cursor-hook-relay.ts +82 -0
- package/scripts/harness-animate.ts +469 -0
- package/scripts/harness-bin.js +77 -0
- package/scripts/harness-core.ts +1 -0
- package/scripts/harness-inspector.ts +439 -0
- package/scripts/harness.ts +2493 -0
- package/src/adapters/agent-session-state.ts +390 -0
- package/src/cli/gateway-record.ts +173 -0
- package/src/codex/live-session.ts +872 -0
- package/src/config/config-core.ts +1359 -0
- package/src/config/secrets-core.ts +170 -0
- package/src/control-plane/agent-realtime-api.ts +2441 -0
- package/src/control-plane/codex-session-stream.ts +392 -0
- package/src/control-plane/codex-telemetry.ts +1325 -0
- package/src/control-plane/lifecycle-hooks.ts +706 -0
- package/src/control-plane/session-summary.ts +380 -0
- package/src/control-plane/status/agent-status-reducer.ts +21 -0
- package/src/control-plane/status/reducer-base.ts +170 -0
- package/src/control-plane/status/reducers/claude-status-reducer.ts +37 -0
- package/src/control-plane/status/reducers/codex-status-reducer.ts +48 -0
- package/src/control-plane/status/reducers/critique-status-reducer.ts +15 -0
- package/src/control-plane/status/reducers/cursor-status-reducer.ts +37 -0
- package/src/control-plane/status/reducers/terminal-status-reducer.ts +15 -0
- package/src/control-plane/status/session-status-engine.ts +76 -0
- package/src/control-plane/stream-client.ts +396 -0
- package/src/control-plane/stream-command-parser.ts +1673 -0
- package/src/control-plane/stream-protocol.ts +1808 -0
- package/src/control-plane/stream-server-background.ts +486 -0
- package/src/control-plane/stream-server-command.ts +2557 -0
- package/src/control-plane/stream-server-connection.ts +234 -0
- package/src/control-plane/stream-server-observed-filter.ts +112 -0
- package/src/control-plane/stream-server-session-runtime.ts +566 -0
- package/src/control-plane/stream-server-state-store.ts +15 -0
- package/src/control-plane/stream-server.ts +3192 -0
- package/src/cursor/managed-hooks.ts +282 -0
- package/src/domain/conversations.ts +414 -0
- package/src/domain/directories.ts +78 -0
- package/src/domain/repositories.ts +123 -0
- package/src/domain/tasks.ts +148 -0
- package/src/domain/workspace.ts +156 -0
- package/src/events/normalized-events.ts +124 -0
- package/src/mux/ansi-integrity.ts +103 -0
- package/src/mux/control-plane-op-queue.ts +212 -0
- package/src/mux/conversation-rail.ts +339 -0
- package/src/mux/double-click.ts +78 -0
- package/src/mux/dual-pane-core.ts +435 -0
- package/src/mux/harness-core-ui.ts +817 -0
- package/src/mux/input-shortcuts.ts +667 -0
- package/src/mux/live-mux/actions-conversation.ts +344 -0
- package/src/mux/live-mux/actions-repository.ts +246 -0
- package/src/mux/live-mux/actions-task.ts +115 -0
- package/src/mux/live-mux/args.ts +142 -0
- package/src/mux/live-mux/command-menu.ts +298 -0
- package/src/mux/live-mux/control-plane-records.ts +546 -0
- package/src/mux/live-mux/conversation-state.ts +188 -0
- package/src/mux/live-mux/directory-resolution.ts +34 -0
- package/src/mux/live-mux/event-mapping.ts +96 -0
- package/src/mux/live-mux/gateway-profiler.ts +152 -0
- package/src/mux/live-mux/gateway-render-trace.ts +177 -0
- package/src/mux/live-mux/gateway-status-timeline.ts +166 -0
- package/src/mux/live-mux/git-parsing.ts +131 -0
- package/src/mux/live-mux/git-snapshot.ts +263 -0
- package/src/mux/live-mux/git-state.ts +136 -0
- package/src/mux/live-mux/global-shortcut-handlers.ts +143 -0
- package/src/mux/live-mux/home-pane-actions.ts +58 -0
- package/src/mux/live-mux/home-pane-drop.ts +44 -0
- package/src/mux/live-mux/home-pane-entity-click.ts +96 -0
- package/src/mux/live-mux/home-pane-pointer.ts +96 -0
- package/src/mux/live-mux/input-forwarding.ts +112 -0
- package/src/mux/live-mux/layout.ts +30 -0
- package/src/mux/live-mux/left-nav-activation.ts +103 -0
- package/src/mux/live-mux/left-nav.ts +85 -0
- package/src/mux/live-mux/left-rail-actions.ts +118 -0
- package/src/mux/live-mux/left-rail-conversation-click.ts +82 -0
- package/src/mux/live-mux/left-rail-pointer.ts +74 -0
- package/src/mux/live-mux/modal-command-menu-handler.ts +101 -0
- package/src/mux/live-mux/modal-conversation-handlers.ts +217 -0
- package/src/mux/live-mux/modal-input-reducers.ts +94 -0
- package/src/mux/live-mux/modal-overlays.ts +287 -0
- package/src/mux/live-mux/modal-pointer.ts +70 -0
- package/src/mux/live-mux/modal-prompt-handlers.ts +187 -0
- package/src/mux/live-mux/modal-task-editor-handler.ts +156 -0
- package/src/mux/live-mux/observed-stream.ts +87 -0
- package/src/mux/live-mux/palette-parsing.ts +128 -0
- package/src/mux/live-mux/pointer-routing.ts +108 -0
- package/src/mux/live-mux/process-usage.ts +53 -0
- package/src/mux/live-mux/project-pane-pointer.ts +44 -0
- package/src/mux/live-mux/rail-layout.ts +244 -0
- package/src/mux/live-mux/render-trace-analysis.ts +213 -0
- package/src/mux/live-mux/render-trace-state.ts +84 -0
- package/src/mux/live-mux/repository-folding.ts +207 -0
- package/src/mux/live-mux/runtime-shutdown.ts +51 -0
- package/src/mux/live-mux/selection.ts +411 -0
- package/src/mux/live-mux/startup-utils.ts +187 -0
- package/src/mux/live-mux/status-timeline-state.ts +82 -0
- package/src/mux/live-mux/task-pane-shortcuts.ts +206 -0
- package/src/mux/live-mux/terminal-palette.ts +79 -0
- package/src/mux/new-thread-prompt.ts +165 -0
- package/src/mux/project-tree.ts +295 -0
- package/src/mux/render-frame.ts +113 -0
- package/src/mux/runtime-wiring.ts +185 -0
- package/src/mux/selector-index.ts +160 -0
- package/src/mux/startup-sequencer.ts +238 -0
- package/src/mux/task-composer.ts +289 -0
- package/src/mux/task-focused-pane.ts +417 -0
- package/src/mux/task-screen-keybindings.ts +539 -0
- package/src/mux/terminal-input-modes.ts +35 -0
- package/src/mux/workspace-path.ts +55 -0
- package/src/mux/workspace-rail-model.ts +701 -0
- package/src/mux/workspace-rail.ts +247 -0
- package/src/perf/perf-core.ts +307 -0
- package/src/pty/pty_host.ts +217 -0
- package/src/pty/session-broker.ts +158 -0
- package/src/recording/terminal-recording.ts +383 -0
- package/src/services/control-plane.ts +567 -0
- package/src/services/conversation-lifecycle.ts +176 -0
- package/src/services/conversation-startup-hydration.ts +47 -0
- package/src/services/directory-hydration.ts +49 -0
- package/src/services/event-persistence.ts +104 -0
- package/src/services/mux-ui-state-persistence.ts +82 -0
- package/src/services/output-load-sampler.ts +231 -0
- package/src/services/process-usage-refresh.ts +88 -0
- package/src/services/recording.ts +75 -0
- package/src/services/render-trace-recorder.ts +177 -0
- package/src/services/runtime-control-actions.ts +123 -0
- package/src/services/runtime-control-plane-ops.ts +131 -0
- package/src/services/runtime-conversation-actions.ts +113 -0
- package/src/services/runtime-conversation-activation.ts +78 -0
- package/src/services/runtime-conversation-starter.ts +171 -0
- package/src/services/runtime-conversation-title-edit.ts +149 -0
- package/src/services/runtime-directory-actions.ts +164 -0
- package/src/services/runtime-envelope-handler.ts +198 -0
- package/src/services/runtime-git-state.ts +92 -0
- package/src/services/runtime-input-pipeline.ts +50 -0
- package/src/services/runtime-input-router.ts +202 -0
- package/src/services/runtime-layout-resize.ts +236 -0
- package/src/services/runtime-left-rail-render.ts +159 -0
- package/src/services/runtime-main-pane-input.ts +230 -0
- package/src/services/runtime-modal-input.ts +119 -0
- package/src/services/runtime-navigation-input.ts +207 -0
- package/src/services/runtime-process-wiring.ts +68 -0
- package/src/services/runtime-rail-input.ts +287 -0
- package/src/services/runtime-render-flush.ts +146 -0
- package/src/services/runtime-render-lifecycle.ts +104 -0
- package/src/services/runtime-render-orchestrator.ts +108 -0
- package/src/services/runtime-render-pipeline.ts +167 -0
- package/src/services/runtime-render-state.ts +72 -0
- package/src/services/runtime-repository-actions.ts +197 -0
- package/src/services/runtime-right-pane-render.ts +132 -0
- package/src/services/runtime-shutdown.ts +79 -0
- package/src/services/runtime-stream-subscriptions.ts +56 -0
- package/src/services/runtime-task-composer-persistence.ts +139 -0
- package/src/services/runtime-task-editor-actions.ts +83 -0
- package/src/services/runtime-task-pane-actions.ts +198 -0
- package/src/services/runtime-task-pane-shortcuts.ts +189 -0
- package/src/services/runtime-task-pane.ts +62 -0
- package/src/services/runtime-workspace-actions.ts +153 -0
- package/src/services/runtime-workspace-observed-events.ts +190 -0
- package/src/services/session-projection-instrumentation.ts +190 -0
- package/src/services/startup-background-probe.ts +91 -0
- package/src/services/startup-background-resume.ts +65 -0
- package/src/services/startup-orchestrator.ts +166 -0
- package/src/services/startup-output-tracker.ts +54 -0
- package/src/services/startup-paint-tracker.ts +115 -0
- package/src/services/startup-persisted-conversation-queue.ts +45 -0
- package/src/services/startup-settled-gate.ts +67 -0
- package/src/services/startup-shutdown.ts +53 -0
- package/src/services/startup-span-tracker.ts +77 -0
- package/src/services/startup-state-hydration.ts +94 -0
- package/src/services/startup-visibility.ts +35 -0
- package/src/services/status-timeline-recorder.ts +144 -0
- package/src/services/task-pane-selection-actions.ts +153 -0
- package/src/services/task-planning-hydration.ts +58 -0
- package/src/services/task-planning-observed-events.ts +89 -0
- package/src/services/workspace-observed-events.ts +113 -0
- package/src/store/control-plane-store-normalize.ts +760 -0
- package/src/store/control-plane-store-types.ts +224 -0
- package/src/store/control-plane-store.ts +2951 -0
- package/src/store/event-store.ts +253 -0
- package/src/store/sqlite.ts +81 -0
- package/src/terminal/compat-matrix.ts +345 -0
- package/src/terminal/differential-checkpoints.ts +132 -0
- package/src/terminal/parity-suite.ts +441 -0
- package/src/terminal/snapshot-oracle.ts +1840 -0
- package/src/ui/conversation-input-forwarder.ts +114 -0
- package/src/ui/conversation-selection-input.ts +103 -0
- package/src/ui/debug-footer-notice.ts +39 -0
- package/src/ui/global-shortcut-input.ts +126 -0
- package/src/ui/input-preflight.ts +68 -0
- package/src/ui/input-token-router.ts +312 -0
- package/src/ui/input.ts +238 -0
- package/src/ui/kit.ts +509 -0
- package/src/ui/left-nav-input.ts +80 -0
- package/src/ui/left-rail-pointer-input.ts +148 -0
- package/src/ui/main-pane-pointer-input.ts +150 -0
- package/src/ui/modals/manager.ts +192 -0
- package/src/ui/mux-theme.ts +529 -0
- package/src/ui/panes/conversation.ts +19 -0
- package/src/ui/panes/home-gridfire.ts +302 -0
- package/src/ui/panes/home.ts +109 -0
- package/src/ui/panes/left-rail.ts +12 -0
- package/src/ui/panes/project.ts +44 -0
- package/src/ui/pointer-routing-input.ts +158 -0
- package/src/ui/repository-fold-input.ts +91 -0
- package/src/ui/screen.ts +210 -0
- package/src/ui/surface.ts +224 -0
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
import { padOrTrimDisplay } from './dual-pane-core.ts';
|
|
3
|
+
import { buildProjectTreeLines } from './project-tree.ts';
|
|
4
|
+
import { wrapTextForColumns } from '../terminal/snapshot-oracle.ts';
|
|
5
|
+
import { formatUiButton } from '../ui/kit.ts';
|
|
6
|
+
|
|
7
|
+
export type ProjectPaneAction = 'conversation.new' | 'project.close';
|
|
8
|
+
export type TaskStatus = 'draft' | 'ready' | 'in-progress' | 'completed';
|
|
9
|
+
export type TaskPaneAction =
|
|
10
|
+
| 'task.create'
|
|
11
|
+
| 'repository.create'
|
|
12
|
+
| 'repository.edit'
|
|
13
|
+
| 'repository.archive'
|
|
14
|
+
| 'task.edit'
|
|
15
|
+
| 'task.delete'
|
|
16
|
+
| 'task.ready'
|
|
17
|
+
| 'task.draft'
|
|
18
|
+
| 'task.complete'
|
|
19
|
+
| 'task.reorder-up'
|
|
20
|
+
| 'task.reorder-down';
|
|
21
|
+
|
|
22
|
+
export interface ProjectPaneSnapshot {
|
|
23
|
+
readonly directoryId: string;
|
|
24
|
+
readonly path: string;
|
|
25
|
+
readonly lines: readonly string[];
|
|
26
|
+
readonly actionLineIndexByKind: {
|
|
27
|
+
readonly conversationNew: number;
|
|
28
|
+
readonly projectClose: number;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ProjectPaneWrappedLine {
|
|
33
|
+
readonly text: string;
|
|
34
|
+
readonly sourceLineIndex: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TaskPaneRepositoryRecord {
|
|
38
|
+
readonly repositoryId: string;
|
|
39
|
+
readonly name: string;
|
|
40
|
+
readonly remoteUrl?: string;
|
|
41
|
+
readonly defaultBranch?: string;
|
|
42
|
+
readonly metadata?: Record<string, unknown>;
|
|
43
|
+
readonly archivedAt: string | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface TaskPaneTaskRecord {
|
|
47
|
+
readonly taskId: string;
|
|
48
|
+
readonly repositoryId: string | null;
|
|
49
|
+
readonly title: string;
|
|
50
|
+
readonly description: string;
|
|
51
|
+
readonly status: TaskStatus;
|
|
52
|
+
readonly orderIndex: number;
|
|
53
|
+
readonly completedAt: string | null;
|
|
54
|
+
readonly createdAt: string;
|
|
55
|
+
readonly updatedAt: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface TaskPaneSnapshotLine {
|
|
59
|
+
readonly text: string;
|
|
60
|
+
readonly taskId: string | null;
|
|
61
|
+
readonly repositoryId: string | null;
|
|
62
|
+
readonly action: TaskPaneAction | null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface TaskPaneSnapshot {
|
|
66
|
+
readonly lines: readonly TaskPaneSnapshotLine[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface TaskPaneActionCell {
|
|
70
|
+
readonly startCol: number;
|
|
71
|
+
readonly endCol: number;
|
|
72
|
+
readonly action: TaskPaneAction;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface TaskPaneView {
|
|
76
|
+
readonly rows: readonly string[];
|
|
77
|
+
readonly taskIds: readonly (string | null)[];
|
|
78
|
+
readonly repositoryIds: readonly (string | null)[];
|
|
79
|
+
readonly actions: readonly (TaskPaneAction | null)[];
|
|
80
|
+
readonly actionCells: readonly (readonly TaskPaneActionCell[] | null)[];
|
|
81
|
+
readonly top: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const PROJECT_PANE_NEW_CONVERSATION_BUTTON_LABEL = formatUiButton({
|
|
85
|
+
label: 'new thread',
|
|
86
|
+
prefixIcon: '+',
|
|
87
|
+
});
|
|
88
|
+
export const PROJECT_PANE_CLOSE_PROJECT_BUTTON_LABEL = formatUiButton({
|
|
89
|
+
label: 'close project',
|
|
90
|
+
prefixIcon: '<',
|
|
91
|
+
});
|
|
92
|
+
export const TASKS_PANE_ADD_TASK_BUTTON_LABEL = formatUiButton({
|
|
93
|
+
label: 'add task',
|
|
94
|
+
prefixIcon: '+',
|
|
95
|
+
});
|
|
96
|
+
export const TASKS_PANE_ADD_REPOSITORY_BUTTON_LABEL = formatUiButton({
|
|
97
|
+
label: 'add repository',
|
|
98
|
+
prefixIcon: '+',
|
|
99
|
+
});
|
|
100
|
+
export const TASKS_PANE_EDIT_REPOSITORY_BUTTON_LABEL = formatUiButton({
|
|
101
|
+
label: 'edit repository',
|
|
102
|
+
prefixIcon: 'e',
|
|
103
|
+
});
|
|
104
|
+
export const TASKS_PANE_ARCHIVE_REPOSITORY_BUTTON_LABEL = formatUiButton({
|
|
105
|
+
label: 'archive repository',
|
|
106
|
+
prefixIcon: 'x',
|
|
107
|
+
});
|
|
108
|
+
export const TASKS_PANE_EDIT_TASK_BUTTON_LABEL = formatUiButton({
|
|
109
|
+
label: 'edit task',
|
|
110
|
+
prefixIcon: 'e',
|
|
111
|
+
});
|
|
112
|
+
export const TASKS_PANE_DELETE_TASK_BUTTON_LABEL = formatUiButton({
|
|
113
|
+
label: 'delete task',
|
|
114
|
+
prefixIcon: 'x',
|
|
115
|
+
});
|
|
116
|
+
export const TASKS_PANE_READY_TASK_BUTTON_LABEL = formatUiButton({
|
|
117
|
+
label: 'mark ready',
|
|
118
|
+
prefixIcon: 'r',
|
|
119
|
+
});
|
|
120
|
+
export const TASKS_PANE_DRAFT_TASK_BUTTON_LABEL = formatUiButton({
|
|
121
|
+
label: 'mark draft',
|
|
122
|
+
prefixIcon: 'd',
|
|
123
|
+
});
|
|
124
|
+
export const TASKS_PANE_COMPLETE_TASK_BUTTON_LABEL = formatUiButton({
|
|
125
|
+
label: 'mark complete',
|
|
126
|
+
prefixIcon: 'c',
|
|
127
|
+
});
|
|
128
|
+
export const TASKS_PANE_REORDER_UP_BUTTON_LABEL = formatUiButton({
|
|
129
|
+
label: 'move up',
|
|
130
|
+
prefixIcon: '^',
|
|
131
|
+
});
|
|
132
|
+
export const TASKS_PANE_REORDER_DOWN_BUTTON_LABEL = formatUiButton({
|
|
133
|
+
label: 'move down',
|
|
134
|
+
prefixIcon: 'v',
|
|
135
|
+
});
|
|
136
|
+
export const TASKS_PANE_FOOTER_EDIT_BUTTON_LABEL = formatUiButton({
|
|
137
|
+
label: 'edit ^E',
|
|
138
|
+
prefixIcon: '✎',
|
|
139
|
+
});
|
|
140
|
+
export const TASKS_PANE_FOOTER_DELETE_BUTTON_LABEL = formatUiButton({
|
|
141
|
+
label: 'delete ^?',
|
|
142
|
+
prefixIcon: '⌫',
|
|
143
|
+
});
|
|
144
|
+
export const TASKS_PANE_FOOTER_COMPLETE_BUTTON_LABEL = formatUiButton({
|
|
145
|
+
label: 'complete ^S',
|
|
146
|
+
prefixIcon: '✓',
|
|
147
|
+
});
|
|
148
|
+
export const TASKS_PANE_FOOTER_DRAFT_BUTTON_LABEL = formatUiButton({
|
|
149
|
+
label: 'draft ^R',
|
|
150
|
+
prefixIcon: '◇',
|
|
151
|
+
});
|
|
152
|
+
export const TASKS_PANE_FOOTER_REPOSITORY_EDIT_BUTTON_LABEL = formatUiButton({
|
|
153
|
+
label: 'repo edit E',
|
|
154
|
+
prefixIcon: '✎',
|
|
155
|
+
});
|
|
156
|
+
export const TASKS_PANE_FOOTER_REPOSITORY_ARCHIVE_BUTTON_LABEL = formatUiButton({
|
|
157
|
+
label: 'repo archive X',
|
|
158
|
+
prefixIcon: '⌫',
|
|
159
|
+
});
|
|
160
|
+
export const CONVERSATION_EDIT_ARCHIVE_BUTTON_LABEL = formatUiButton({
|
|
161
|
+
label: 'archive thread',
|
|
162
|
+
prefixIcon: 'x',
|
|
163
|
+
});
|
|
164
|
+
export const NEW_THREAD_MODAL_CODEX_BUTTON = formatUiButton({
|
|
165
|
+
label: 'codex',
|
|
166
|
+
prefixIcon: '◆',
|
|
167
|
+
});
|
|
168
|
+
export const NEW_THREAD_MODAL_CLAUDE_BUTTON = formatUiButton({
|
|
169
|
+
label: 'claude',
|
|
170
|
+
prefixIcon: '◇',
|
|
171
|
+
});
|
|
172
|
+
export const NEW_THREAD_MODAL_CURSOR_BUTTON = formatUiButton({
|
|
173
|
+
label: 'cursor',
|
|
174
|
+
prefixIcon: '◈',
|
|
175
|
+
});
|
|
176
|
+
export const NEW_THREAD_MODAL_TERMINAL_BUTTON = formatUiButton({
|
|
177
|
+
label: 'terminal',
|
|
178
|
+
prefixIcon: '▣',
|
|
179
|
+
});
|
|
180
|
+
export const NEW_THREAD_MODAL_CRITIQUE_BUTTON = formatUiButton({
|
|
181
|
+
label: 'critique',
|
|
182
|
+
prefixIcon: '▤',
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const GOLDEN_RATIO = (1 + Math.sqrt(5)) / 2;
|
|
186
|
+
|
|
187
|
+
function parseIsoTimestampMs(value: string | null | undefined): number {
|
|
188
|
+
if (value === null || value === undefined) {
|
|
189
|
+
return Number.NaN;
|
|
190
|
+
}
|
|
191
|
+
return Date.parse(value);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function formatRelativeIsoTime(nowMs: number, value: string | null | undefined): string {
|
|
195
|
+
const ts = parseIsoTimestampMs(value);
|
|
196
|
+
if (!Number.isFinite(ts)) {
|
|
197
|
+
return 'unknown';
|
|
198
|
+
}
|
|
199
|
+
const deltaSeconds = Math.max(0, Math.floor((nowMs - ts) / 1000));
|
|
200
|
+
if (deltaSeconds < 60) {
|
|
201
|
+
return `${String(deltaSeconds)}s ago`;
|
|
202
|
+
}
|
|
203
|
+
const deltaMinutes = Math.floor(deltaSeconds / 60);
|
|
204
|
+
if (deltaMinutes < 60) {
|
|
205
|
+
return `${String(deltaMinutes)}m ago`;
|
|
206
|
+
}
|
|
207
|
+
const deltaHours = Math.floor(deltaMinutes / 60);
|
|
208
|
+
if (deltaHours < 24) {
|
|
209
|
+
return `${String(deltaHours)}h ago`;
|
|
210
|
+
}
|
|
211
|
+
const deltaDays = Math.floor(deltaHours / 24);
|
|
212
|
+
return `${String(deltaDays)}d ago`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function sortedRepositoryList<T extends TaskPaneRepositoryRecord>(
|
|
216
|
+
repositories: ReadonlyMap<string, T>,
|
|
217
|
+
): readonly T[] {
|
|
218
|
+
return [...repositories.values()]
|
|
219
|
+
.filter((repository) => repository.archivedAt === null)
|
|
220
|
+
.sort((left, right) => {
|
|
221
|
+
const leftPriority = repositoryHomePriority(left);
|
|
222
|
+
const rightPriority = repositoryHomePriority(right);
|
|
223
|
+
if (leftPriority !== null && rightPriority !== null && leftPriority !== rightPriority) {
|
|
224
|
+
return leftPriority - rightPriority;
|
|
225
|
+
}
|
|
226
|
+
if (leftPriority !== null && rightPriority === null) {
|
|
227
|
+
return -1;
|
|
228
|
+
}
|
|
229
|
+
if (leftPriority === null && rightPriority !== null) {
|
|
230
|
+
return 1;
|
|
231
|
+
}
|
|
232
|
+
const nameCompare = left.name.localeCompare(right.name);
|
|
233
|
+
if (nameCompare !== 0) {
|
|
234
|
+
return nameCompare;
|
|
235
|
+
}
|
|
236
|
+
return left.repositoryId.localeCompare(right.repositoryId);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function sortTasksByOrder<T extends TaskPaneTaskRecord>(tasks: readonly T[]): readonly T[] {
|
|
241
|
+
return [...tasks].sort((left, right) => {
|
|
242
|
+
if (left.orderIndex !== right.orderIndex) {
|
|
243
|
+
return left.orderIndex - right.orderIndex;
|
|
244
|
+
}
|
|
245
|
+
const leftCreatedAt = parseIsoTimestampMs(left.createdAt);
|
|
246
|
+
const rightCreatedAt = parseIsoTimestampMs(right.createdAt);
|
|
247
|
+
if (
|
|
248
|
+
Number.isFinite(leftCreatedAt) &&
|
|
249
|
+
Number.isFinite(rightCreatedAt) &&
|
|
250
|
+
leftCreatedAt !== rightCreatedAt
|
|
251
|
+
) {
|
|
252
|
+
return leftCreatedAt - rightCreatedAt;
|
|
253
|
+
}
|
|
254
|
+
return left.taskId.localeCompare(right.taskId);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function taskStatusSortRank(status: TaskStatus): number {
|
|
259
|
+
if (status === 'in-progress') {
|
|
260
|
+
return 0;
|
|
261
|
+
}
|
|
262
|
+
if (status === 'ready') {
|
|
263
|
+
return 1;
|
|
264
|
+
}
|
|
265
|
+
if (status === 'draft') {
|
|
266
|
+
return 2;
|
|
267
|
+
}
|
|
268
|
+
return 3;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function taskStatusGlyph(status: TaskStatus): string {
|
|
272
|
+
if (status === 'in-progress') {
|
|
273
|
+
return '▶';
|
|
274
|
+
}
|
|
275
|
+
if (status === 'ready') {
|
|
276
|
+
return '◆';
|
|
277
|
+
}
|
|
278
|
+
if (status === 'draft') {
|
|
279
|
+
return '◇';
|
|
280
|
+
}
|
|
281
|
+
return '✓';
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function repositoryRemoteLabel(remoteUrl: string | undefined): string {
|
|
285
|
+
const normalized = (remoteUrl ?? '').trim();
|
|
286
|
+
if (normalized.length === 0) {
|
|
287
|
+
return '(no remote)';
|
|
288
|
+
}
|
|
289
|
+
const match = /github\.com[/:]([^/\s]+\/[^/\s]+?)(?:\.git)?(?:\/)?$/iu.exec(normalized);
|
|
290
|
+
if (match !== null) {
|
|
291
|
+
return `github.com/${match[1] as string}`;
|
|
292
|
+
}
|
|
293
|
+
return normalized.replace(/^https?:\/\//iu, '').replace(/\.git$/iu, '');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function sortTasksForHomePane<T extends TaskPaneTaskRecord>(
|
|
297
|
+
tasks: readonly T[],
|
|
298
|
+
): readonly T[] {
|
|
299
|
+
return [...tasks].sort((left, right) => {
|
|
300
|
+
const statusCompare = taskStatusSortRank(left.status) - taskStatusSortRank(right.status);
|
|
301
|
+
if (statusCompare !== 0) {
|
|
302
|
+
return statusCompare;
|
|
303
|
+
}
|
|
304
|
+
if (left.orderIndex !== right.orderIndex) {
|
|
305
|
+
return left.orderIndex - right.orderIndex;
|
|
306
|
+
}
|
|
307
|
+
const leftCreatedAt = parseIsoTimestampMs(left.createdAt);
|
|
308
|
+
const rightCreatedAt = parseIsoTimestampMs(right.createdAt);
|
|
309
|
+
if (
|
|
310
|
+
Number.isFinite(leftCreatedAt) &&
|
|
311
|
+
Number.isFinite(rightCreatedAt) &&
|
|
312
|
+
leftCreatedAt !== rightCreatedAt
|
|
313
|
+
) {
|
|
314
|
+
return leftCreatedAt - rightCreatedAt;
|
|
315
|
+
}
|
|
316
|
+
return left.taskId.localeCompare(right.taskId);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function buildProjectPaneWrappedLines(
|
|
321
|
+
snapshot: ProjectPaneSnapshot,
|
|
322
|
+
cols: number,
|
|
323
|
+
): readonly ProjectPaneWrappedLine[] {
|
|
324
|
+
const safeCols = Math.max(1, cols);
|
|
325
|
+
const wrapped: ProjectPaneWrappedLine[] = [];
|
|
326
|
+
for (let lineIndex = 0; lineIndex < snapshot.lines.length; lineIndex += 1) {
|
|
327
|
+
const line = snapshot.lines[lineIndex]!;
|
|
328
|
+
const segments = wrapTextForColumns(line, safeCols);
|
|
329
|
+
for (const segment of segments) {
|
|
330
|
+
wrapped.push({
|
|
331
|
+
text: segment,
|
|
332
|
+
sourceLineIndex: lineIndex,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (wrapped.length === 0) {
|
|
337
|
+
wrapped.push({
|
|
338
|
+
text: '',
|
|
339
|
+
sourceLineIndex: -1,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
return wrapped;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function truncateLabel(value: string, maxChars: number): string {
|
|
346
|
+
const normalized = value.trim();
|
|
347
|
+
const safeMaxChars = Math.max(2, Math.floor(maxChars));
|
|
348
|
+
if (normalized.length <= safeMaxChars) {
|
|
349
|
+
return normalized;
|
|
350
|
+
}
|
|
351
|
+
return `${normalized.slice(0, safeMaxChars - 1)}…`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function parseMetadataNumber(value: unknown): number | null {
|
|
355
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
return Math.max(0, Math.floor(value));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function parseMetadataTimestamp(value: unknown): string | null {
|
|
362
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
return Number.isFinite(Date.parse(value)) ? value : null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function repositoryHomePriority(repository: TaskPaneRepositoryRecord): number | null {
|
|
369
|
+
const metadata = repository.metadata;
|
|
370
|
+
if (metadata === undefined) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
const raw = metadata['homePriority'];
|
|
374
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
if (!Number.isInteger(raw) || raw < 0) {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
return raw;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function repositoryCommitCountLabel(repository: TaskPaneRepositoryRecord): string {
|
|
384
|
+
const metadata = repository.metadata;
|
|
385
|
+
if (metadata === undefined) {
|
|
386
|
+
return '?c';
|
|
387
|
+
}
|
|
388
|
+
const commitCount = parseMetadataNumber(metadata['commitCount']);
|
|
389
|
+
if (commitCount === null) {
|
|
390
|
+
return '?c';
|
|
391
|
+
}
|
|
392
|
+
return `${String(commitCount)}c`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function formatCompactRelativeIsoTime(nowMs: number, value: string | null): string {
|
|
396
|
+
const ts = parseIsoTimestampMs(value);
|
|
397
|
+
if (!Number.isFinite(ts)) {
|
|
398
|
+
return '?';
|
|
399
|
+
}
|
|
400
|
+
const deltaSeconds = Math.max(0, Math.floor((nowMs - ts) / 1000));
|
|
401
|
+
if (deltaSeconds < 60) {
|
|
402
|
+
return `${String(deltaSeconds)}s`;
|
|
403
|
+
}
|
|
404
|
+
const deltaMinutes = Math.floor(deltaSeconds / 60);
|
|
405
|
+
if (deltaMinutes < 60) {
|
|
406
|
+
return `${String(deltaMinutes)}m`;
|
|
407
|
+
}
|
|
408
|
+
const deltaHours = Math.floor(deltaMinutes / 60);
|
|
409
|
+
if (deltaHours < 24) {
|
|
410
|
+
return `${String(deltaHours)}h`;
|
|
411
|
+
}
|
|
412
|
+
const deltaDays = Math.floor(deltaHours / 24);
|
|
413
|
+
return `${String(deltaDays)}d`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function repositoryLastCommitAgeLabel(repository: TaskPaneRepositoryRecord, nowMs: number): string {
|
|
417
|
+
const metadata = repository.metadata;
|
|
418
|
+
if (metadata === undefined) {
|
|
419
|
+
return '?';
|
|
420
|
+
}
|
|
421
|
+
const lastCommitAt = parseMetadataTimestamp(metadata['lastCommitAt']);
|
|
422
|
+
return formatCompactRelativeIsoTime(nowMs, lastCommitAt);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function fillLine(width: number, glyph: string): string {
|
|
426
|
+
if (width <= 0) {
|
|
427
|
+
return '';
|
|
428
|
+
}
|
|
429
|
+
return glyph.repeat(width);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function normalizeTaskPaneRow(text: string, cols: number): string {
|
|
433
|
+
const innerCols = Math.max(1, cols - 2);
|
|
434
|
+
return `│${padOrTrimDisplay(text, innerCols)}│`;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export function resolveGoldenModalSize(
|
|
438
|
+
viewportCols: number,
|
|
439
|
+
viewportRows: number,
|
|
440
|
+
options: {
|
|
441
|
+
readonly preferredHeight: number;
|
|
442
|
+
readonly minWidth: number;
|
|
443
|
+
readonly maxWidth: number;
|
|
444
|
+
},
|
|
445
|
+
): { width: number; height: number } {
|
|
446
|
+
const safeViewportCols = Math.max(1, Math.floor(viewportCols));
|
|
447
|
+
const safeViewportRows = Math.max(1, Math.floor(viewportRows));
|
|
448
|
+
const maxHeight = Math.max(1, safeViewportRows - 2);
|
|
449
|
+
const height = Math.max(1, Math.min(Math.floor(options.preferredHeight), maxHeight));
|
|
450
|
+
const maxWidth = Math.max(
|
|
451
|
+
options.minWidth,
|
|
452
|
+
Math.min(Math.floor(options.maxWidth), Math.max(1, safeViewportCols - 2)),
|
|
453
|
+
);
|
|
454
|
+
const targetWidth = Math.round(height * GOLDEN_RATIO);
|
|
455
|
+
const width = Math.max(Math.floor(options.minWidth), Math.min(targetWidth, maxWidth));
|
|
456
|
+
return {
|
|
457
|
+
width,
|
|
458
|
+
height,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export function buildProjectPaneSnapshot(directoryId: string, path: string): ProjectPaneSnapshot {
|
|
463
|
+
const projectName = basename(path) || path;
|
|
464
|
+
const actionLineIndexByKind = {
|
|
465
|
+
conversationNew: 3,
|
|
466
|
+
projectClose: 4,
|
|
467
|
+
} as const;
|
|
468
|
+
return {
|
|
469
|
+
directoryId,
|
|
470
|
+
path,
|
|
471
|
+
lines: [
|
|
472
|
+
`project ${projectName}`,
|
|
473
|
+
`path ${path}`,
|
|
474
|
+
'',
|
|
475
|
+
PROJECT_PANE_NEW_CONVERSATION_BUTTON_LABEL,
|
|
476
|
+
PROJECT_PANE_CLOSE_PROJECT_BUTTON_LABEL,
|
|
477
|
+
'',
|
|
478
|
+
...buildProjectTreeLines(path),
|
|
479
|
+
],
|
|
480
|
+
actionLineIndexByKind,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export function buildProjectPaneRows(
|
|
485
|
+
snapshot: ProjectPaneSnapshot,
|
|
486
|
+
cols: number,
|
|
487
|
+
paneRows: number,
|
|
488
|
+
scrollTop: number,
|
|
489
|
+
): { rows: readonly string[]; top: number } {
|
|
490
|
+
const safeCols = Math.max(1, cols);
|
|
491
|
+
const safeRows = Math.max(1, paneRows);
|
|
492
|
+
const wrappedLines = buildProjectPaneWrappedLines(snapshot, safeCols);
|
|
493
|
+
const maxTop = Math.max(0, wrappedLines.length - safeRows);
|
|
494
|
+
const nextTop = Math.max(0, Math.min(maxTop, scrollTop));
|
|
495
|
+
const viewport = wrappedLines.slice(nextTop, nextTop + safeRows);
|
|
496
|
+
while (viewport.length < safeRows) {
|
|
497
|
+
viewport.push({
|
|
498
|
+
text: '',
|
|
499
|
+
sourceLineIndex: -1,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
rows: viewport.map((row) => padOrTrimDisplay(row.text, safeCols)),
|
|
504
|
+
top: nextTop,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export function projectPaneActionAtRow(
|
|
509
|
+
snapshot: ProjectPaneSnapshot,
|
|
510
|
+
cols: number,
|
|
511
|
+
paneRows: number,
|
|
512
|
+
scrollTop: number,
|
|
513
|
+
rowIndex: number,
|
|
514
|
+
): ProjectPaneAction | null {
|
|
515
|
+
const safeRows = Math.max(1, paneRows);
|
|
516
|
+
const wrappedLines = buildProjectPaneWrappedLines(snapshot, cols);
|
|
517
|
+
const maxTop = Math.max(0, wrappedLines.length - safeRows);
|
|
518
|
+
const nextTop = Math.max(0, Math.min(maxTop, scrollTop));
|
|
519
|
+
const normalizedRow = Math.max(0, Math.min(safeRows - 1, rowIndex));
|
|
520
|
+
const line = wrappedLines[nextTop + normalizedRow]!;
|
|
521
|
+
if (line.sourceLineIndex === snapshot.actionLineIndexByKind.conversationNew) {
|
|
522
|
+
return 'conversation.new';
|
|
523
|
+
}
|
|
524
|
+
if (line.sourceLineIndex === snapshot.actionLineIndexByKind.projectClose) {
|
|
525
|
+
return 'project.close';
|
|
526
|
+
}
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export function buildTaskPaneSnapshot(
|
|
531
|
+
repositories: ReadonlyMap<string, TaskPaneRepositoryRecord>,
|
|
532
|
+
tasks: ReadonlyMap<string, TaskPaneTaskRecord>,
|
|
533
|
+
selectedTaskId: string | null,
|
|
534
|
+
selectedRepositoryId: string | null,
|
|
535
|
+
nowMs: number,
|
|
536
|
+
notice: string | null,
|
|
537
|
+
): TaskPaneSnapshot {
|
|
538
|
+
const activeRepositories = sortedRepositoryList(repositories);
|
|
539
|
+
const repositoryNameById = new Map<string, string>(
|
|
540
|
+
activeRepositories.map((repository) => [repository.repositoryId, repository.name] as const),
|
|
541
|
+
);
|
|
542
|
+
const orderedTasks = sortTasksForHomePane([...tasks.values()]);
|
|
543
|
+
const activeTasks = orderedTasks.filter((task) => task.status !== 'completed');
|
|
544
|
+
const completedTasks = orderedTasks.filter((task) => task.status === 'completed');
|
|
545
|
+
const effectiveSelectedTaskId =
|
|
546
|
+
(selectedTaskId !== null && tasks.has(selectedTaskId) ? selectedTaskId : null) ??
|
|
547
|
+
activeTasks[0]?.taskId ??
|
|
548
|
+
orderedTasks[0]?.taskId ??
|
|
549
|
+
null;
|
|
550
|
+
const effectiveSelectedRepositoryId =
|
|
551
|
+
(selectedRepositoryId !== null && repositories.has(selectedRepositoryId)
|
|
552
|
+
? selectedRepositoryId
|
|
553
|
+
: null) ??
|
|
554
|
+
activeRepositories[0]?.repositoryId ??
|
|
555
|
+
null;
|
|
556
|
+
const lines: TaskPaneSnapshotLine[] = [];
|
|
557
|
+
const push = (
|
|
558
|
+
text: string,
|
|
559
|
+
taskId: string | null = null,
|
|
560
|
+
repositoryId: string | null = null,
|
|
561
|
+
action: TaskPaneAction | null = null,
|
|
562
|
+
): void => {
|
|
563
|
+
lines.push({
|
|
564
|
+
text,
|
|
565
|
+
taskId,
|
|
566
|
+
repositoryId,
|
|
567
|
+
action,
|
|
568
|
+
});
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
if (notice !== null) {
|
|
572
|
+
push(` NOTICE: ${truncateLabel(notice, 68)}`);
|
|
573
|
+
push('');
|
|
574
|
+
}
|
|
575
|
+
push(
|
|
576
|
+
' REPOSITORIES R add drag prioritize',
|
|
577
|
+
null,
|
|
578
|
+
null,
|
|
579
|
+
'repository.create',
|
|
580
|
+
);
|
|
581
|
+
push(` ${fillLine(74, '─')}`);
|
|
582
|
+
if (activeRepositories.length === 0) {
|
|
583
|
+
push(' no repositories');
|
|
584
|
+
} else {
|
|
585
|
+
for (const repository of activeRepositories) {
|
|
586
|
+
const selected = repository.repositoryId === effectiveSelectedRepositoryId ? '▸' : ' ';
|
|
587
|
+
const repositoryName =
|
|
588
|
+
repository.name.trim().length === 0 ? '(unnamed repository)' : repository.name.trim();
|
|
589
|
+
const remoteLabel = repositoryRemoteLabel(repository.remoteUrl);
|
|
590
|
+
const branch = repository.defaultBranch?.trim() ?? '';
|
|
591
|
+
const branchLabel = branch.length === 0 ? 'main' : branch;
|
|
592
|
+
const updatedLabel = repositoryLastCommitAgeLabel(repository, nowMs);
|
|
593
|
+
const commitCountLabel = repositoryCommitCountLabel(repository);
|
|
594
|
+
const row = ` ${selected} ${truncateLabel(repositoryName, 14).padEnd(14)} ${truncateLabel(remoteLabel, 30).padEnd(30)} ${truncateLabel(branchLabel, 6).padEnd(6)} ${updatedLabel.padStart(3)} ${commitCountLabel.padStart(4)}`;
|
|
595
|
+
push(row, null, repository.repositoryId);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
push('');
|
|
599
|
+
push(
|
|
600
|
+
' TASKS A add E edit X archive',
|
|
601
|
+
null,
|
|
602
|
+
null,
|
|
603
|
+
'task.create',
|
|
604
|
+
);
|
|
605
|
+
push(` ${fillLine(74, '─')}`);
|
|
606
|
+
if (orderedTasks.length === 0) {
|
|
607
|
+
push(' no tasks');
|
|
608
|
+
} else {
|
|
609
|
+
for (const task of orderedTasks) {
|
|
610
|
+
const selected = task.taskId === effectiveSelectedTaskId ? '▸' : ' ';
|
|
611
|
+
const repositoryName =
|
|
612
|
+
(task.repositoryId !== null ? repositoryNameById.get(task.repositoryId) : null) ??
|
|
613
|
+
'(missing repo)';
|
|
614
|
+
const taskOrderLabel = `#${String(Math.max(1, task.orderIndex + 1))}`;
|
|
615
|
+
const row = ` ${selected} ${taskStatusGlyph(task.status)} ${truncateLabel(task.title, 38).padEnd(38)} ${truncateLabel(repositoryName, 14).padEnd(14)} ${taskOrderLabel}`;
|
|
616
|
+
push(row, task.taskId, task.repositoryId);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (completedTasks.length > 0) {
|
|
620
|
+
push('');
|
|
621
|
+
push(
|
|
622
|
+
` COMPLETED: ${String(completedTasks.length)} UPDATED ${formatRelativeIsoTime(nowMs, completedTasks[0]?.updatedAt)}`,
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
return {
|
|
626
|
+
lines,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
export function buildTaskPaneRows(
|
|
631
|
+
snapshot: TaskPaneSnapshot,
|
|
632
|
+
cols: number,
|
|
633
|
+
paneRows: number,
|
|
634
|
+
scrollTop: number,
|
|
635
|
+
): TaskPaneView {
|
|
636
|
+
const safeCols = Math.max(1, cols);
|
|
637
|
+
const safeRows = Math.max(1, paneRows);
|
|
638
|
+
const tooSmallForFrame = safeCols < 4 || safeRows < 4;
|
|
639
|
+
if (tooSmallForFrame) {
|
|
640
|
+
const maxTop = Math.max(0, snapshot.lines.length - safeRows);
|
|
641
|
+
const nextTop = Math.max(0, Math.min(maxTop, scrollTop));
|
|
642
|
+
const viewport = snapshot.lines.slice(nextTop, nextTop + safeRows);
|
|
643
|
+
while (viewport.length < safeRows) {
|
|
644
|
+
viewport.push({
|
|
645
|
+
text: '',
|
|
646
|
+
taskId: null,
|
|
647
|
+
repositoryId: null,
|
|
648
|
+
action: null,
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
return {
|
|
652
|
+
rows: viewport.map((row) => padOrTrimDisplay(row.text, safeCols)),
|
|
653
|
+
taskIds: viewport.map((row) => row.taskId),
|
|
654
|
+
repositoryIds: viewport.map((row) => row.repositoryId),
|
|
655
|
+
actions: viewport.map((row) => row.action),
|
|
656
|
+
actionCells: viewport.map(() => null),
|
|
657
|
+
top: nextTop,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const fixedRows = 5;
|
|
662
|
+
const contentRows = Math.max(1, safeRows - fixedRows);
|
|
663
|
+
const maxTop = Math.max(0, snapshot.lines.length - contentRows);
|
|
664
|
+
const nextTop = Math.max(0, Math.min(maxTop, scrollTop));
|
|
665
|
+
const viewport = snapshot.lines.slice(nextTop, nextTop + contentRows);
|
|
666
|
+
while (viewport.length < contentRows) {
|
|
667
|
+
viewport.push({
|
|
668
|
+
text: '',
|
|
669
|
+
taskId: null,
|
|
670
|
+
repositoryId: null,
|
|
671
|
+
action: null,
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const rows: string[] = [];
|
|
676
|
+
const taskIds: Array<string | null> = [];
|
|
677
|
+
const repositoryIds: Array<string | null> = [];
|
|
678
|
+
const actions: Array<TaskPaneAction | null> = [];
|
|
679
|
+
const actionCells: Array<readonly TaskPaneActionCell[] | null> = [];
|
|
680
|
+
|
|
681
|
+
const pushRow = (
|
|
682
|
+
text: string,
|
|
683
|
+
taskId: string | null = null,
|
|
684
|
+
repositoryId: string | null = null,
|
|
685
|
+
action: TaskPaneAction | null = null,
|
|
686
|
+
cells: readonly TaskPaneActionCell[] | null = null,
|
|
687
|
+
): void => {
|
|
688
|
+
rows.push(padOrTrimDisplay(text, safeCols));
|
|
689
|
+
taskIds.push(taskId);
|
|
690
|
+
repositoryIds.push(repositoryId);
|
|
691
|
+
actions.push(action);
|
|
692
|
+
actionCells.push(cells);
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
const innerCols = safeCols - 2;
|
|
696
|
+
const topInner = `─ Home ${fillLine(Math.max(0, innerCols - 7), '─')}`;
|
|
697
|
+
pushRow(`┌${padOrTrimDisplay(topInner, innerCols)}┐`);
|
|
698
|
+
for (const row of viewport) {
|
|
699
|
+
pushRow(normalizeTaskPaneRow(row.text, safeCols), row.taskId, row.repositoryId, row.action);
|
|
700
|
+
}
|
|
701
|
+
pushRow(`├${fillLine(innerCols, '─')}┤`);
|
|
702
|
+
|
|
703
|
+
const repositoryFooterContent = ` ${TASKS_PANE_FOOTER_REPOSITORY_EDIT_BUTTON_LABEL} ${TASKS_PANE_FOOTER_REPOSITORY_ARCHIVE_BUTTON_LABEL}`;
|
|
704
|
+
const repositoryFooterInner = padOrTrimDisplay(repositoryFooterContent, innerCols);
|
|
705
|
+
const repositoryFooterCells: TaskPaneActionCell[] = [];
|
|
706
|
+
const repositoryFooterMappings: ReadonlyArray<{ label: string; action: TaskPaneAction }> = [
|
|
707
|
+
{
|
|
708
|
+
label: TASKS_PANE_FOOTER_REPOSITORY_EDIT_BUTTON_LABEL,
|
|
709
|
+
action: 'repository.edit',
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
label: TASKS_PANE_FOOTER_REPOSITORY_ARCHIVE_BUTTON_LABEL,
|
|
713
|
+
action: 'repository.archive',
|
|
714
|
+
},
|
|
715
|
+
];
|
|
716
|
+
for (const mapping of repositoryFooterMappings) {
|
|
717
|
+
const start = repositoryFooterInner.indexOf(mapping.label);
|
|
718
|
+
if (start < 0) {
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
repositoryFooterCells.push({
|
|
722
|
+
startCol: 1 + start,
|
|
723
|
+
endCol: start + mapping.label.length,
|
|
724
|
+
action: mapping.action,
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
pushRow(`│${repositoryFooterInner}│`, null, null, null, repositoryFooterCells);
|
|
728
|
+
|
|
729
|
+
const taskFooterContent = ` ${TASKS_PANE_FOOTER_EDIT_BUTTON_LABEL} ${TASKS_PANE_FOOTER_DELETE_BUTTON_LABEL} ${TASKS_PANE_FOOTER_COMPLETE_BUTTON_LABEL} ${TASKS_PANE_FOOTER_DRAFT_BUTTON_LABEL}`;
|
|
730
|
+
const taskFooterInner = padOrTrimDisplay(taskFooterContent, innerCols);
|
|
731
|
+
const taskFooterCells: TaskPaneActionCell[] = [];
|
|
732
|
+
const taskFooterMappings: ReadonlyArray<{ label: string; action: TaskPaneAction }> = [
|
|
733
|
+
{
|
|
734
|
+
label: TASKS_PANE_FOOTER_EDIT_BUTTON_LABEL,
|
|
735
|
+
action: 'task.edit',
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
label: TASKS_PANE_FOOTER_DELETE_BUTTON_LABEL,
|
|
739
|
+
action: 'task.delete',
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
label: TASKS_PANE_FOOTER_COMPLETE_BUTTON_LABEL,
|
|
743
|
+
action: 'task.complete',
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
label: TASKS_PANE_FOOTER_DRAFT_BUTTON_LABEL,
|
|
747
|
+
action: 'task.draft',
|
|
748
|
+
},
|
|
749
|
+
];
|
|
750
|
+
for (const mapping of taskFooterMappings) {
|
|
751
|
+
const start = taskFooterInner.indexOf(mapping.label);
|
|
752
|
+
if (start < 0) {
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
taskFooterCells.push({
|
|
756
|
+
startCol: 1 + start,
|
|
757
|
+
endCol: start + mapping.label.length,
|
|
758
|
+
action: mapping.action,
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
pushRow(`│${taskFooterInner}│`, null, null, null, taskFooterCells);
|
|
762
|
+
pushRow(`└${fillLine(innerCols, '─')}┘`);
|
|
763
|
+
|
|
764
|
+
return {
|
|
765
|
+
rows,
|
|
766
|
+
taskIds,
|
|
767
|
+
repositoryIds,
|
|
768
|
+
actions,
|
|
769
|
+
actionCells,
|
|
770
|
+
top: nextTop,
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
export function taskPaneActionAtRow(view: TaskPaneView, rowIndex: number): TaskPaneAction | null {
|
|
775
|
+
if (view.actions.length === 0) {
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
const normalizedRow = Math.max(0, Math.min(view.actions.length - 1, rowIndex));
|
|
779
|
+
return view.actions[normalizedRow] ?? null;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
export function taskPaneActionAtCell(
|
|
783
|
+
view: TaskPaneView,
|
|
784
|
+
rowIndex: number,
|
|
785
|
+
colIndex: number,
|
|
786
|
+
): TaskPaneAction | null {
|
|
787
|
+
if (view.rows.length === 0) {
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
const normalizedRow = Math.max(0, Math.min(view.rows.length - 1, rowIndex));
|
|
791
|
+
const normalizedCol = Math.max(0, Math.floor(colIndex));
|
|
792
|
+
const hitboxes = view.actionCells[normalizedRow] ?? null;
|
|
793
|
+
if (hitboxes !== null) {
|
|
794
|
+
for (const hitbox of hitboxes) {
|
|
795
|
+
if (normalizedCol >= hitbox.startCol && normalizedCol <= hitbox.endCol) {
|
|
796
|
+
return hitbox.action;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
return taskPaneActionAtRow(view, normalizedRow);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
export function taskPaneTaskIdAtRow(view: TaskPaneView, rowIndex: number): string | null {
|
|
804
|
+
if (view.taskIds.length === 0) {
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
const normalizedRow = Math.max(0, Math.min(view.taskIds.length - 1, rowIndex));
|
|
808
|
+
return view.taskIds[normalizedRow] ?? null;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
export function taskPaneRepositoryIdAtRow(view: TaskPaneView, rowIndex: number): string | null {
|
|
812
|
+
if (view.repositoryIds.length === 0) {
|
|
813
|
+
return null;
|
|
814
|
+
}
|
|
815
|
+
const normalizedRow = Math.max(0, Math.min(view.repositoryIds.length - 1, rowIndex));
|
|
816
|
+
return view.repositoryIds[normalizedRow] ?? null;
|
|
817
|
+
}
|