@jmoyers/harness 0.1.10 → 0.1.20
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/README.md +31 -35
- package/package.json +31 -11
- package/packages/harness-ai/src/anthropic-protocol.ts +68 -68
- package/packages/harness-ai/src/stream-text.ts +13 -91
- package/packages/harness-ui/src/frame-primitives.ts +158 -0
- package/packages/harness-ui/src/index.ts +18 -0
- package/packages/harness-ui/src/interaction/conversation-input-forwarder.ts +221 -0
- package/packages/harness-ui/src/interaction/conversation-selection-input.ts +213 -0
- package/packages/harness-ui/src/interaction/global-shortcut-input.ts +172 -0
- package/{src/ui → packages/harness-ui/src/interaction}/input-preflight.ts +10 -12
- package/{src/ui → packages/harness-ui/src/interaction}/input-token-router.ts +120 -69
- package/packages/harness-ui/src/interaction/input.ts +420 -0
- package/packages/harness-ui/src/interaction/left-nav-input.ts +166 -0
- package/{src/ui → packages/harness-ui/src/interaction}/main-pane-pointer-input.ts +91 -23
- package/{src/ui → packages/harness-ui/src/interaction}/pointer-routing-input.ts +112 -48
- package/packages/harness-ui/src/interaction/rail-pointer-input.ts +62 -0
- package/packages/harness-ui/src/interaction/repository-fold-input.ts +118 -0
- package/packages/harness-ui/src/kit.ts +476 -0
- package/packages/harness-ui/src/layout.ts +238 -0
- package/{src/ui/modals/manager.ts → packages/harness-ui/src/modal-manager.ts} +94 -64
- package/{src/ui → packages/harness-ui/src}/screen.ts +53 -26
- package/packages/harness-ui/src/surface.ts +252 -0
- package/packages/harness-ui/src/text-layout.ts +210 -0
- package/packages/nim-core/src/contracts.ts +239 -0
- package/packages/nim-core/src/event-store.ts +299 -0
- package/packages/nim-core/src/events.ts +53 -0
- package/packages/nim-core/src/index.ts +9 -0
- package/packages/nim-core/src/provider-router.ts +129 -0
- package/packages/nim-core/src/providers/anthropic-driver.ts +291 -0
- package/packages/nim-core/src/runtime-factory.ts +49 -0
- package/packages/nim-core/src/runtime.ts +1797 -0
- package/packages/nim-core/src/session-store.ts +516 -0
- package/packages/nim-core/src/telemetry.ts +48 -0
- package/packages/nim-test-tui/src/index.ts +150 -0
- package/packages/nim-ui-core/src/index.ts +1 -0
- package/packages/nim-ui-core/src/projection.ts +87 -0
- package/scripts/codex-live-mux-runtime.ts +2 -3721
- package/scripts/control-plane-daemon.ts +24 -2
- package/scripts/harness-bin.js +5 -0
- package/scripts/harness-commands.ts +300 -0
- package/scripts/harness-runtime.ts +82 -0
- package/scripts/harness.ts +33 -3007
- package/scripts/nim-tui-smoke.ts +748 -0
- package/src/cli/auth/runtime.ts +948 -0
- package/src/cli/default-gateway-pointer.ts +193 -0
- package/src/cli/gateway/runtime.ts +1872 -0
- package/src/cli/parsing/flags.ts +23 -0
- package/src/cli/parsing/session.ts +42 -0
- package/src/cli/runtime/context.ts +193 -0
- package/src/cli/runtime-app/application.ts +392 -0
- package/src/cli/runtime-infra/gateway-control.ts +729 -0
- package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
- package/src/cli/workflows/runtime.ts +965 -0
- package/src/clients/tui/left-rail-interactions.ts +519 -0
- package/src/clients/tui/main-pane-interactions.ts +509 -0
- package/src/clients/tui/modal-input-routing.ts +71 -0
- package/src/clients/tui/render-snapshot-adapter.ts +88 -0
- package/src/clients/web/synced-selectors.ts +132 -0
- package/src/codex/live-session.ts +82 -29
- package/src/config/config-core.ts +361 -10
- package/src/config/harness-paths.ts +4 -7
- package/src/config/harness-runtime-migration.ts +142 -19
- package/src/config/harness.config.template.jsonc +33 -0
- package/src/config/secrets-core.ts +92 -4
- package/src/control-plane/agent-realtime-api.ts +82 -427
- package/src/control-plane/prompt/thread-title-namer.ts +49 -23
- package/src/control-plane/session-summary.ts +10 -81
- package/src/control-plane/status/reducer-base.ts +12 -12
- package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
- package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
- package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
- package/src/control-plane/stream-client.ts +12 -2
- package/src/control-plane/stream-command-parser.ts +83 -143
- package/src/control-plane/stream-protocol.ts +53 -37
- package/src/control-plane/stream-server-background.ts +18 -2
- package/src/control-plane/stream-server-command.ts +376 -69
- package/src/control-plane/stream-server-session-runtime.ts +3 -2
- package/src/control-plane/stream-server.ts +943 -80
- package/src/control-plane/stream-session-runtime-types.ts +41 -0
- package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
- package/src/core/state/observed-stream-cursor.ts +43 -0
- package/src/core/state/synced-observed-state.ts +273 -0
- package/src/core/store/harness-synced-store.ts +81 -0
- package/src/diff/budget.ts +136 -0
- package/src/diff/build.ts +289 -0
- package/src/diff/chunker.ts +146 -0
- package/src/diff/git-invoke.ts +315 -0
- package/src/diff/git-parse.ts +472 -0
- package/src/diff/hash.ts +70 -0
- package/src/diff/index.ts +24 -0
- package/src/diff/normalize.ts +134 -0
- package/src/diff/types.ts +178 -0
- package/src/diff-ui/args.ts +346 -0
- package/src/diff-ui/commands.ts +123 -0
- package/src/diff-ui/finder.ts +94 -0
- package/src/diff-ui/highlight.ts +127 -0
- package/src/diff-ui/index.ts +2 -0
- package/src/diff-ui/model.ts +141 -0
- package/src/diff-ui/pager.ts +412 -0
- package/src/diff-ui/render.ts +337 -0
- package/src/diff-ui/runtime.ts +379 -0
- package/src/diff-ui/state.ts +224 -0
- package/src/diff-ui/types.ts +236 -0
- package/src/domain/conversations.ts +11 -7
- package/src/domain/workspace.ts +76 -4
- package/src/mux/control-plane-op-queue.ts +93 -7
- package/src/mux/conversation-rail.ts +28 -71
- package/src/mux/dual-pane-core.ts +13 -13
- package/src/mux/harness-core-ui.ts +313 -42
- package/src/mux/input-shortcuts.ts +22 -112
- package/src/mux/keybinding-catalog.ts +340 -0
- package/src/mux/keybinding-registry.ts +103 -0
- package/src/mux/live-mux/command-menu-open-in.ts +280 -0
- package/src/mux/live-mux/command-menu.ts +167 -4
- package/src/mux/live-mux/conversation-state.ts +13 -0
- package/src/mux/live-mux/directory-resolution.ts +1 -1
- package/src/mux/live-mux/git-parsing.ts +16 -0
- package/src/mux/live-mux/git-snapshot.ts +33 -2
- package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
- package/src/mux/live-mux/home-pane-drop.ts +1 -1
- package/src/mux/live-mux/home-pane-pointer.ts +10 -0
- package/src/mux/live-mux/input-forwarding.ts +59 -2
- package/src/mux/live-mux/left-nav-activation.ts +124 -7
- package/src/mux/live-mux/left-nav.ts +35 -0
- package/src/mux/live-mux/link-click.ts +292 -0
- package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
- package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
- package/src/mux/live-mux/modal-input-reducers.ts +106 -8
- package/src/mux/live-mux/modal-overlays.ts +210 -31
- package/src/mux/live-mux/modal-pointer.ts +3 -7
- package/src/mux/live-mux/modal-prompt-handlers.ts +107 -1
- package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
- package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
- package/src/mux/live-mux/pointer-routing.ts +5 -2
- package/src/mux/live-mux/project-pane-pointer.ts +8 -0
- package/src/mux/live-mux/rail-layout.ts +33 -30
- package/src/mux/live-mux/release-notes.ts +383 -0
- package/src/mux/live-mux/render-trace-analysis.ts +52 -7
- package/src/mux/live-mux/repository-folding.ts +3 -0
- package/src/mux/live-mux/selection.ts +0 -4
- package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
- package/src/mux/project-pane-github-review.ts +271 -0
- package/src/mux/render-frame.ts +4 -0
- package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
- package/src/mux/task-composer.ts +21 -14
- package/src/mux/task-focused-pane.ts +118 -117
- package/src/mux/task-screen-keybindings.ts +19 -82
- package/src/mux/workspace-rail-model.ts +270 -104
- package/src/mux/workspace-rail.ts +45 -22
- package/src/pty/session-broker.ts +1 -1
- package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
- package/src/services/control-plane.ts +50 -32
- package/src/services/conversation-lifecycle.ts +118 -87
- package/src/services/conversation-startup-hydration.ts +20 -12
- package/src/services/directory-hydration.ts +21 -16
- package/src/services/event-persistence.ts +7 -0
- package/src/services/left-rail-pointer-handler.ts +329 -0
- package/src/services/mux-ui-state-persistence.ts +5 -1
- package/src/services/recording.ts +34 -26
- package/src/services/runtime-command-menu-agent-tools.ts +1 -1
- package/src/services/runtime-control-actions.ts +79 -61
- package/src/services/runtime-control-plane-ops.ts +122 -83
- package/src/services/runtime-conversation-actions.ts +40 -26
- package/src/services/runtime-conversation-activation.ts +82 -30
- package/src/services/runtime-conversation-starter.ts +80 -48
- package/src/services/runtime-conversation-title-edit.ts +91 -80
- package/src/services/runtime-envelope-handler.ts +107 -105
- package/src/services/runtime-git-state.ts +42 -29
- package/src/services/runtime-layout-resize.ts +3 -1
- package/src/services/runtime-left-rail-render.ts +99 -63
- package/src/services/runtime-nim-cli-session.ts +438 -0
- package/src/services/runtime-nim-session.ts +705 -0
- package/src/services/runtime-nim-tool-bridge.ts +141 -0
- package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
- package/src/services/runtime-process-wiring.ts +29 -36
- package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
- package/src/services/runtime-render-flush.ts +63 -70
- package/src/services/runtime-render-lifecycle.ts +65 -64
- package/src/services/runtime-render-orchestrator.ts +55 -45
- package/src/services/runtime-render-pipeline.ts +106 -103
- package/src/services/runtime-render-state.ts +62 -49
- package/src/services/runtime-repository-actions.ts +97 -70
- package/src/services/runtime-right-pane-render.ts +80 -53
- package/src/services/runtime-shutdown.ts +38 -35
- package/src/services/runtime-stream-subscriptions.ts +35 -27
- package/src/services/runtime-task-composer-persistence.ts +71 -59
- package/src/services/runtime-task-composer-snapshot.ts +14 -0
- package/src/services/runtime-task-editor-actions.ts +46 -29
- package/src/services/runtime-task-pane-actions.ts +220 -134
- package/src/services/runtime-task-pane-shortcuts.ts +323 -123
- package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
- package/src/services/runtime-workspace-observed-events.ts +33 -184
- package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
- package/src/services/session-diagnostics-store.ts +217 -0
- package/src/services/startup-background-resume.ts +26 -21
- package/src/services/startup-orchestrator.ts +16 -13
- package/src/services/startup-paint-tracker.ts +29 -21
- package/src/services/startup-persisted-conversation-queue.ts +19 -13
- package/src/services/startup-settled-gate.ts +25 -15
- package/src/services/startup-shutdown.ts +18 -22
- package/src/services/startup-state-hydration.ts +44 -34
- package/src/services/startup-visibility.ts +12 -4
- package/src/services/task-pane-selection-actions.ts +89 -72
- package/src/services/task-planning-hydration.ts +24 -18
- package/src/services/task-planning-observed-events.ts +50 -52
- package/src/services/workspace-observed-events.ts +66 -63
- package/src/storage/storage-lifecycle-core.ts +438 -0
- package/src/store/control-plane-store-normalize.ts +33 -242
- package/src/store/control-plane-store-types.ts +1 -35
- package/src/store/control-plane-store.ts +396 -56
- package/src/store/event-store.ts +397 -3
- package/src/terminal/snapshot-oracle.ts +207 -94
- package/src/ui/mux-theme.ts +112 -8
- package/src/ui/panes/home-gridfire.ts +40 -31
- package/src/ui/panes/home.ts +10 -2
- package/src/ui/panes/nim.ts +315 -0
- package/src/mux/live-mux/actions-task.ts +0 -115
- package/src/mux/live-mux/left-rail-actions.ts +0 -118
- package/src/mux/live-mux/left-rail-conversation-click.ts +0 -82
- package/src/mux/live-mux/left-rail-pointer.ts +0 -74
- package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
- package/src/services/runtime-directory-actions.ts +0 -164
- package/src/services/runtime-input-pipeline.ts +0 -50
- package/src/services/runtime-input-router.ts +0 -189
- package/src/services/runtime-main-pane-input.ts +0 -230
- package/src/services/runtime-modal-input.ts +0 -119
- package/src/services/runtime-navigation-input.ts +0 -197
- package/src/services/runtime-rail-input.ts +0 -278
- package/src/services/runtime-task-pane.ts +0 -62
- package/src/services/runtime-workspace-actions.ts +0 -158
- package/src/ui/conversation-input-forwarder.ts +0 -114
- package/src/ui/conversation-selection-input.ts +0 -103
- package/src/ui/global-shortcut-input.ts +0 -89
- package/src/ui/input.ts +0 -238
- package/src/ui/kit.ts +0 -509
- package/src/ui/left-nav-input.ts +0 -80
- package/src/ui/left-rail-pointer-input.ts +0 -148
- package/src/ui/repository-fold-input.ts +0 -91
- package/src/ui/surface.ts +0 -224
package/src/mux/task-composer.ts
CHANGED
|
@@ -258,32 +258,39 @@ export function taskComposerMoveVertical(
|
|
|
258
258
|
|
|
259
259
|
export function taskComposerVisibleLines(
|
|
260
260
|
buffer: TaskComposerBuffer,
|
|
261
|
-
cursorToken = '
|
|
261
|
+
cursorToken = '█',
|
|
262
|
+
cursorVisible = true,
|
|
262
263
|
): readonly string[] {
|
|
263
264
|
const normalized = normalizeTaskComposerBuffer(buffer);
|
|
265
|
+
if (!cursorVisible) {
|
|
266
|
+
return normalized.text.split('\n');
|
|
267
|
+
}
|
|
264
268
|
const textWithCursor =
|
|
265
|
-
normalized.
|
|
266
|
-
|
|
267
|
-
|
|
269
|
+
normalized.cursor >= normalized.text.length
|
|
270
|
+
? `${normalized.text}${cursorToken}`
|
|
271
|
+
: normalized.text.slice(0, normalized.cursor) +
|
|
272
|
+
cursorToken +
|
|
273
|
+
normalized.text.slice(normalized.cursor + 1);
|
|
268
274
|
return textWithCursor.split('\n');
|
|
269
275
|
}
|
|
270
276
|
|
|
271
|
-
export function taskComposerTextFromTaskFields(title: string,
|
|
272
|
-
if (
|
|
273
|
-
return
|
|
277
|
+
export function taskComposerTextFromTaskFields(title: string, body: string): string {
|
|
278
|
+
if (body.length > 0) {
|
|
279
|
+
return body;
|
|
274
280
|
}
|
|
275
|
-
return
|
|
281
|
+
return title;
|
|
276
282
|
}
|
|
277
283
|
|
|
278
284
|
export function taskFieldsFromComposerText(text: string): {
|
|
279
|
-
readonly title: string;
|
|
280
|
-
readonly
|
|
285
|
+
readonly title: string | null;
|
|
286
|
+
readonly body: string;
|
|
281
287
|
} {
|
|
282
|
-
const
|
|
288
|
+
const normalized = text.replace(/\r\n/gu, '\n');
|
|
289
|
+
const lines = normalized.split('\n');
|
|
283
290
|
const firstLine = lines[0] ?? '';
|
|
284
|
-
const
|
|
291
|
+
const title = firstLine.trim();
|
|
285
292
|
return {
|
|
286
|
-
title:
|
|
287
|
-
|
|
293
|
+
title: title.length === 0 ? null : title,
|
|
294
|
+
body: normalized,
|
|
288
295
|
};
|
|
289
296
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { padOrTrimDisplay } from './dual-pane-core.ts';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
import type { TaskStatus } from './harness-core-ui.ts';
|
|
3
|
+
import { UiKit } from '../../packages/harness-ui/src/kit.ts';
|
|
4
|
+
import { WrappingInputRenderer } from '../../packages/harness-ui/src/text-layout.ts';
|
|
5
|
+
import { taskComposerTextFromTaskFields, type TaskComposerBuffer } from './task-composer.ts';
|
|
6
|
+
|
|
7
|
+
const UI_KIT = new UiKit();
|
|
8
|
+
const WRAPPING_INPUT_RENDERER = new WrappingInputRenderer();
|
|
9
9
|
|
|
10
10
|
export type TaskFocusedPaneAction =
|
|
11
11
|
| 'repository.dropdown.toggle'
|
|
@@ -24,6 +24,7 @@ interface ActionCell {
|
|
|
24
24
|
export interface TaskFocusedPaneRepositoryRecord {
|
|
25
25
|
readonly repositoryId: string;
|
|
26
26
|
readonly name: string;
|
|
27
|
+
readonly metadata?: Record<string, unknown>;
|
|
27
28
|
readonly archivedAt: string | null;
|
|
28
29
|
}
|
|
29
30
|
|
|
@@ -31,7 +32,7 @@ export interface TaskFocusedPaneTaskRecord {
|
|
|
31
32
|
readonly taskId: string;
|
|
32
33
|
readonly repositoryId: string | null;
|
|
33
34
|
readonly title: string;
|
|
34
|
-
readonly
|
|
35
|
+
readonly body: string;
|
|
35
36
|
readonly status: TaskStatus;
|
|
36
37
|
readonly orderIndex: number;
|
|
37
38
|
readonly createdAt: string;
|
|
@@ -58,6 +59,7 @@ interface BuildTaskFocusedPaneOptions {
|
|
|
58
59
|
readonly cols: number;
|
|
59
60
|
readonly rows: number;
|
|
60
61
|
readonly scrollTop: number;
|
|
62
|
+
readonly cursorVisible?: boolean;
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
interface PaneLine {
|
|
@@ -70,6 +72,7 @@ interface PaneLine {
|
|
|
70
72
|
|
|
71
73
|
export interface TaskFocusedPaneView {
|
|
72
74
|
readonly rows: readonly string[];
|
|
75
|
+
readonly plainRows?: readonly string[];
|
|
73
76
|
readonly taskIds: readonly (string | null)[];
|
|
74
77
|
readonly repositoryIds: readonly (string | null)[];
|
|
75
78
|
readonly actions: readonly (TaskFocusedPaneAction | null)[];
|
|
@@ -78,52 +81,71 @@ export interface TaskFocusedPaneView {
|
|
|
78
81
|
readonly selectedRepositoryId: string | null;
|
|
79
82
|
}
|
|
80
83
|
|
|
81
|
-
const READY_CHIP_LABEL = formatUiButton({
|
|
82
|
-
label: 'ready',
|
|
83
|
-
prefixIcon: 'r',
|
|
84
|
-
});
|
|
85
|
-
const DRAFT_CHIP_LABEL = formatUiButton({
|
|
86
|
-
label: 'queued',
|
|
87
|
-
prefixIcon: 'd',
|
|
88
|
-
});
|
|
89
|
-
const COMPLETE_CHIP_LABEL = formatUiButton({
|
|
90
|
-
label: 'complete',
|
|
91
|
-
prefixIcon: 'c',
|
|
92
|
-
});
|
|
93
|
-
|
|
94
84
|
function sortedRepositories(
|
|
95
85
|
repositories: ReadonlyMap<string, TaskFocusedPaneRepositoryRecord>,
|
|
96
86
|
): readonly TaskFocusedPaneRepositoryRecord[] {
|
|
97
|
-
return [...repositories.values()]
|
|
98
|
-
.filter((entry) => entry.archivedAt === null)
|
|
99
|
-
.sort(
|
|
100
|
-
(left, right) =>
|
|
101
|
-
left.name.localeCompare(right.name) || left.repositoryId.localeCompare(right.repositoryId),
|
|
102
|
-
);
|
|
87
|
+
return [...repositories.values()].filter((repository) => repository.archivedAt === null);
|
|
103
88
|
}
|
|
104
89
|
|
|
105
90
|
function parseIsoMs(value: string): number {
|
|
106
91
|
return Date.parse(value);
|
|
107
92
|
}
|
|
108
93
|
|
|
109
|
-
function
|
|
94
|
+
function taskStatusSortRank(status: TaskStatus): number {
|
|
95
|
+
if (status === 'in-progress') {
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
if (status === 'ready') {
|
|
99
|
+
return 1;
|
|
100
|
+
}
|
|
101
|
+
if (status === 'draft') {
|
|
102
|
+
return 2;
|
|
103
|
+
}
|
|
104
|
+
return 3;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function taskStatusGroupLabel(status: TaskStatus): string {
|
|
108
|
+
if (status === 'in-progress') {
|
|
109
|
+
return 'in prog';
|
|
110
|
+
}
|
|
111
|
+
if (status === 'ready') {
|
|
112
|
+
return 'ready';
|
|
113
|
+
}
|
|
114
|
+
if (status === 'draft') {
|
|
115
|
+
return 'draft';
|
|
116
|
+
}
|
|
117
|
+
return 'complete';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function compareTasksByOrder(
|
|
121
|
+
left: TaskFocusedPaneTaskRecord,
|
|
122
|
+
right: TaskFocusedPaneTaskRecord,
|
|
123
|
+
): number {
|
|
124
|
+
if (left.orderIndex !== right.orderIndex) {
|
|
125
|
+
return left.orderIndex - right.orderIndex;
|
|
126
|
+
}
|
|
127
|
+
const leftTs = parseIsoMs(left.createdAt);
|
|
128
|
+
const rightTs = parseIsoMs(right.createdAt);
|
|
129
|
+
const leftFinite = Number.isFinite(leftTs);
|
|
130
|
+
const rightFinite = Number.isFinite(rightTs);
|
|
131
|
+
if (leftFinite && rightFinite && leftTs !== rightTs) {
|
|
132
|
+
return leftTs - rightTs;
|
|
133
|
+
}
|
|
134
|
+
if (leftFinite !== rightFinite) {
|
|
135
|
+
return leftFinite ? -1 : 1;
|
|
136
|
+
}
|
|
137
|
+
return left.taskId.localeCompare(right.taskId);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function sortTasksForDisplay(
|
|
110
141
|
tasks: readonly TaskFocusedPaneTaskRecord[],
|
|
111
142
|
): readonly TaskFocusedPaneTaskRecord[] {
|
|
112
143
|
return [...tasks].sort((left, right) => {
|
|
113
|
-
|
|
114
|
-
|
|
144
|
+
const statusCompare = taskStatusSortRank(left.status) - taskStatusSortRank(right.status);
|
|
145
|
+
if (statusCompare !== 0) {
|
|
146
|
+
return statusCompare;
|
|
115
147
|
}
|
|
116
|
-
|
|
117
|
-
const rightTs = parseIsoMs(right.createdAt);
|
|
118
|
-
const leftFinite = Number.isFinite(leftTs);
|
|
119
|
-
const rightFinite = Number.isFinite(rightTs);
|
|
120
|
-
if (leftFinite && rightFinite && leftTs !== rightTs) {
|
|
121
|
-
return leftTs - rightTs;
|
|
122
|
-
}
|
|
123
|
-
if (leftFinite !== rightFinite) {
|
|
124
|
-
return leftFinite ? -1 : 1;
|
|
125
|
-
}
|
|
126
|
-
return left.taskId.localeCompare(right.taskId);
|
|
148
|
+
return compareTasksByOrder(left, right);
|
|
127
149
|
});
|
|
128
150
|
}
|
|
129
151
|
|
|
@@ -151,63 +173,26 @@ function truncate(text: string, max: number): string {
|
|
|
151
173
|
return `${text.slice(0, safeMax - 1)}…`;
|
|
152
174
|
}
|
|
153
175
|
|
|
154
|
-
function composeRowWithRightChips(
|
|
155
|
-
left: string,
|
|
156
|
-
width: number,
|
|
157
|
-
chips: readonly { label: string; action: TaskFocusedPaneAction }[],
|
|
158
|
-
): { readonly text: string; readonly cells: readonly ActionCell[] } {
|
|
159
|
-
const joined = chips.map((chip) => chip.label).join(' ');
|
|
160
|
-
if (joined.length === 0 || joined.length >= width) {
|
|
161
|
-
return {
|
|
162
|
-
text: padOrTrimDisplay(left, width),
|
|
163
|
-
cells: [],
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
const startCol = Math.max(0, width - joined.length);
|
|
167
|
-
const leftMax = Math.max(0, startCol - 1);
|
|
168
|
-
const leftText = padOrTrimDisplay(truncate(left, leftMax), leftMax);
|
|
169
|
-
const gap = width - leftText.length - joined.length;
|
|
170
|
-
let cursor = leftText.length + Math.max(0, gap);
|
|
171
|
-
const cells: ActionCell[] = [];
|
|
172
|
-
const parts: string[] = [leftText, ' '.repeat(Math.max(0, gap))];
|
|
173
|
-
for (let idx = 0; idx < chips.length; idx += 1) {
|
|
174
|
-
const chip = chips[idx]!;
|
|
175
|
-
parts.push(chip.label);
|
|
176
|
-
cells.push({
|
|
177
|
-
startCol: cursor,
|
|
178
|
-
endCol: cursor + chip.label.length - 1,
|
|
179
|
-
action: chip.action,
|
|
180
|
-
});
|
|
181
|
-
cursor += chip.label.length;
|
|
182
|
-
if (idx < chips.length - 1) {
|
|
183
|
-
parts.push(' ');
|
|
184
|
-
cursor += 1;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
return {
|
|
188
|
-
text: padOrTrimDisplay(parts.join(''), width),
|
|
189
|
-
cells,
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
|
|
193
176
|
function taskBufferFromRecord(
|
|
194
177
|
task: TaskFocusedPaneTaskRecord,
|
|
195
178
|
overrides: ReadonlyMap<string, TaskComposerBuffer>,
|
|
196
179
|
): TaskComposerBuffer {
|
|
180
|
+
const text = taskComposerTextFromTaskFields(task.title, task.body);
|
|
197
181
|
return (
|
|
198
182
|
overrides.get(task.taskId) ?? {
|
|
199
|
-
text
|
|
200
|
-
cursor:
|
|
183
|
+
text,
|
|
184
|
+
cursor: text.length,
|
|
201
185
|
}
|
|
202
186
|
);
|
|
203
187
|
}
|
|
204
188
|
|
|
205
189
|
function taskPreviewText(task: TaskFocusedPaneTaskRecord): string {
|
|
206
|
-
const summary = task.
|
|
207
|
-
|
|
208
|
-
|
|
190
|
+
const summary = task.body.split('\n')[0] ?? '';
|
|
191
|
+
const trimmed = summary.trim();
|
|
192
|
+
if (trimmed.length > 0) {
|
|
193
|
+
return trimmed;
|
|
209
194
|
}
|
|
210
|
-
return
|
|
195
|
+
return task.title;
|
|
211
196
|
}
|
|
212
197
|
|
|
213
198
|
export function buildTaskFocusedPaneView(
|
|
@@ -224,7 +209,7 @@ export function buildTaskFocusedPaneView(
|
|
|
224
209
|
repositories[0]?.repositoryId ??
|
|
225
210
|
null;
|
|
226
211
|
|
|
227
|
-
const scopedTasks =
|
|
212
|
+
const scopedTasks = sortTasksForDisplay(
|
|
228
213
|
[...options.tasks.values()].filter((task) => task.repositoryId === selectedRepositoryId),
|
|
229
214
|
);
|
|
230
215
|
|
|
@@ -251,7 +236,7 @@ export function buildTaskFocusedPaneView(
|
|
|
251
236
|
? 'select repository'
|
|
252
237
|
: (repositories.find((entry) => entry.repositoryId === selectedRepositoryId)?.name ??
|
|
253
238
|
'(missing)');
|
|
254
|
-
const repositoryButton =
|
|
239
|
+
const repositoryButton = UI_KIT.formatButton({
|
|
255
240
|
label: truncate(selectedRepositoryName, Math.max(8, safeCols - 16)),
|
|
256
241
|
suffixIcon: 'v',
|
|
257
242
|
});
|
|
@@ -291,53 +276,68 @@ export function buildTaskFocusedPaneView(
|
|
|
291
276
|
} else if (scopedTasks.length === 0) {
|
|
292
277
|
push(' no tasks yet for this repository');
|
|
293
278
|
} else {
|
|
279
|
+
const statusCounts = new Map<TaskStatus, number>();
|
|
280
|
+
for (const task of scopedTasks) {
|
|
281
|
+
statusCounts.set(task.status, (statusCounts.get(task.status) ?? 0) + 1);
|
|
282
|
+
}
|
|
294
283
|
push(` tasks (${String(scopedTasks.length)})`);
|
|
284
|
+
let previousStatus: TaskStatus | null = null;
|
|
295
285
|
for (let index = 0; index < scopedTasks.length; index += 1) {
|
|
296
286
|
const task = scopedTasks[index]!;
|
|
287
|
+
if (task.status !== previousStatus) {
|
|
288
|
+
if (previousStatus !== null) {
|
|
289
|
+
push('');
|
|
290
|
+
}
|
|
291
|
+
push(
|
|
292
|
+
` ${statusGlyph(task.status)} ${taskStatusGroupLabel(task.status)} · ${String(statusCounts.get(task.status) ?? 0)}`,
|
|
293
|
+
);
|
|
294
|
+
previousStatus = task.status;
|
|
295
|
+
}
|
|
297
296
|
const focused =
|
|
298
297
|
options.editorTarget.kind === 'task' && options.editorTarget.taskId === task.taskId;
|
|
299
|
-
const leftLabel = ` ${focused ? '▸' : ' '} ${statusGlyph(task.status)} ${truncate(taskPreviewText(task), Math.max(8, safeCols - 24))}`;
|
|
300
|
-
const chips =
|
|
301
|
-
task.status === 'completed'
|
|
302
|
-
? []
|
|
303
|
-
: [
|
|
304
|
-
{ label: READY_CHIP_LABEL, action: 'task.status.ready' as const },
|
|
305
|
-
{ label: DRAFT_CHIP_LABEL, action: 'task.status.draft' as const },
|
|
306
|
-
{ label: COMPLETE_CHIP_LABEL, action: 'task.status.complete' as const },
|
|
307
|
-
];
|
|
308
|
-
const composed = composeRowWithRightChips(leftLabel, safeCols, chips);
|
|
309
|
-
push(composed.text, task.taskId, selectedRepositoryId, 'task.focus', composed.cells);
|
|
310
|
-
|
|
311
298
|
if (focused) {
|
|
312
299
|
const editBuffer = taskBufferFromRecord(task, options.taskBufferById);
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
300
|
+
const editorInnerWidth = Math.max(1, safeCols - 4);
|
|
301
|
+
const editorPrefix = `${statusGlyph(task.status)} `;
|
|
302
|
+
const linesWithCursor = WRAPPING_INPUT_RENDERER.renderLines({
|
|
303
|
+
buffer: editBuffer,
|
|
304
|
+
width: editorInnerWidth,
|
|
305
|
+
linePrefix: editorPrefix,
|
|
306
|
+
cursorToken: '█',
|
|
307
|
+
cursorVisible: options.cursorVisible ?? true,
|
|
308
|
+
});
|
|
309
|
+
push(` ┌${'─'.repeat(editorInnerWidth)}┐`);
|
|
317
310
|
for (const line of linesWithCursor) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
task.taskId,
|
|
321
|
-
selectedRepositoryId,
|
|
322
|
-
'task.focus',
|
|
323
|
-
);
|
|
311
|
+
const content = padOrTrimDisplay(line, editorInnerWidth);
|
|
312
|
+
push(` │${content}│`, task.taskId, selectedRepositoryId, 'task.focus');
|
|
324
313
|
}
|
|
314
|
+
push(` └${'─'.repeat(editorInnerWidth)}┘`);
|
|
315
|
+
continue;
|
|
325
316
|
}
|
|
317
|
+
|
|
318
|
+
const leftLabel = ` ${statusGlyph(task.status)} ${truncate(taskPreviewText(task), Math.max(8, safeCols - 6))}`;
|
|
319
|
+
push(leftLabel, task.taskId, selectedRepositoryId, 'task.focus');
|
|
326
320
|
}
|
|
327
321
|
}
|
|
328
322
|
|
|
329
323
|
push('');
|
|
330
324
|
const draftFocused = options.editorTarget.kind === 'draft';
|
|
331
325
|
push(` draft ${draftFocused ? '(editing)' : '(saved)'}`);
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
326
|
+
const draftInnerWidth = Math.max(1, safeCols - 4);
|
|
327
|
+
push(` ┌${'─'.repeat(draftInnerWidth)}┐`);
|
|
328
|
+
const draftLines = WRAPPING_INPUT_RENDERER.renderLines({
|
|
329
|
+
buffer: options.draftBuffer,
|
|
330
|
+
width: draftInnerWidth,
|
|
331
|
+
cursorToken: '█',
|
|
332
|
+
cursorVisible: draftFocused && (options.cursorVisible ?? true),
|
|
333
|
+
});
|
|
337
334
|
for (const line of draftLines) {
|
|
338
|
-
|
|
335
|
+
const content = padOrTrimDisplay(line, draftInnerWidth);
|
|
336
|
+
push(` │${content}│`);
|
|
339
337
|
}
|
|
340
|
-
push(
|
|
338
|
+
push(` └${'─'.repeat(draftInnerWidth)}┘`);
|
|
339
|
+
push(' enter ready tab draft shift+enter newline');
|
|
340
|
+
push(' alt+g repos ctrl+up/down reorder');
|
|
341
341
|
|
|
342
342
|
const maxTop = Math.max(0, lines.length - safeRows);
|
|
343
343
|
const top = Math.max(0, Math.min(maxTop, options.scrollTop));
|
|
@@ -353,6 +353,7 @@ export function buildTaskFocusedPaneView(
|
|
|
353
353
|
}
|
|
354
354
|
return {
|
|
355
355
|
rows: viewport.map((line) => line.text),
|
|
356
|
+
plainRows: viewport.map((line) => line.text),
|
|
356
357
|
taskIds: viewport.map((line) => line.taskId),
|
|
357
358
|
repositoryIds: viewport.map((line) => line.repositoryId),
|
|
358
359
|
actions: viewport.map((line) => line.action),
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_TASK_SCREEN_KEYBINDINGS_RAW,
|
|
3
|
+
TASK_SCREEN_KEYBINDING_ACTION_ORDER,
|
|
4
|
+
type TaskScreenKeybindingAction,
|
|
5
|
+
} from './keybinding-registry.ts';
|
|
6
|
+
|
|
7
|
+
export { DEFAULT_TASK_SCREEN_KEYBINDINGS_RAW };
|
|
8
|
+
export type { TaskScreenKeybindingAction };
|
|
9
|
+
|
|
1
10
|
interface KeyStroke {
|
|
2
11
|
readonly key: string;
|
|
3
12
|
readonly ctrl: boolean;
|
|
@@ -11,87 +20,7 @@ interface ParsedBinding {
|
|
|
11
20
|
readonly originalText: string;
|
|
12
21
|
}
|
|
13
22
|
|
|
14
|
-
|
|
15
|
-
| 'mux.home.repo.dropdown.toggle'
|
|
16
|
-
| 'mux.home.repo.next'
|
|
17
|
-
| 'mux.home.repo.previous'
|
|
18
|
-
| 'mux.home.task.submit'
|
|
19
|
-
| 'mux.home.task.queue'
|
|
20
|
-
| 'mux.home.task.newline'
|
|
21
|
-
| 'mux.home.task.status.ready'
|
|
22
|
-
| 'mux.home.task.status.draft'
|
|
23
|
-
| 'mux.home.task.status.complete'
|
|
24
|
-
| 'mux.home.task.reorder.up'
|
|
25
|
-
| 'mux.home.task.reorder.down'
|
|
26
|
-
| 'mux.home.editor.cursor.left'
|
|
27
|
-
| 'mux.home.editor.cursor.right'
|
|
28
|
-
| 'mux.home.editor.cursor.up'
|
|
29
|
-
| 'mux.home.editor.cursor.down'
|
|
30
|
-
| 'mux.home.editor.line.start'
|
|
31
|
-
| 'mux.home.editor.line.end'
|
|
32
|
-
| 'mux.home.editor.word.left'
|
|
33
|
-
| 'mux.home.editor.word.right'
|
|
34
|
-
| 'mux.home.editor.delete.backward'
|
|
35
|
-
| 'mux.home.editor.delete.forward'
|
|
36
|
-
| 'mux.home.editor.delete.word.backward'
|
|
37
|
-
| 'mux.home.editor.delete.line.start'
|
|
38
|
-
| 'mux.home.editor.delete.line.end';
|
|
39
|
-
|
|
40
|
-
const ACTION_ORDER: readonly TaskScreenKeybindingAction[] = [
|
|
41
|
-
'mux.home.repo.dropdown.toggle',
|
|
42
|
-
'mux.home.repo.next',
|
|
43
|
-
'mux.home.repo.previous',
|
|
44
|
-
'mux.home.task.submit',
|
|
45
|
-
'mux.home.task.queue',
|
|
46
|
-
'mux.home.task.newline',
|
|
47
|
-
'mux.home.task.status.ready',
|
|
48
|
-
'mux.home.task.status.draft',
|
|
49
|
-
'mux.home.task.status.complete',
|
|
50
|
-
'mux.home.task.reorder.up',
|
|
51
|
-
'mux.home.task.reorder.down',
|
|
52
|
-
'mux.home.editor.cursor.left',
|
|
53
|
-
'mux.home.editor.cursor.right',
|
|
54
|
-
'mux.home.editor.cursor.up',
|
|
55
|
-
'mux.home.editor.cursor.down',
|
|
56
|
-
'mux.home.editor.line.start',
|
|
57
|
-
'mux.home.editor.line.end',
|
|
58
|
-
'mux.home.editor.word.left',
|
|
59
|
-
'mux.home.editor.word.right',
|
|
60
|
-
'mux.home.editor.delete.backward',
|
|
61
|
-
'mux.home.editor.delete.forward',
|
|
62
|
-
'mux.home.editor.delete.word.backward',
|
|
63
|
-
'mux.home.editor.delete.line.start',
|
|
64
|
-
'mux.home.editor.delete.line.end',
|
|
65
|
-
] as const;
|
|
66
|
-
|
|
67
|
-
export const DEFAULT_TASK_SCREEN_KEYBINDINGS_RAW: Readonly<
|
|
68
|
-
Record<TaskScreenKeybindingAction, readonly string[]>
|
|
69
|
-
> = {
|
|
70
|
-
'mux.home.repo.dropdown.toggle': ['ctrl+g'],
|
|
71
|
-
'mux.home.repo.next': ['ctrl+n'],
|
|
72
|
-
'mux.home.repo.previous': ['ctrl+p'],
|
|
73
|
-
'mux.home.task.submit': ['enter'],
|
|
74
|
-
'mux.home.task.queue': ['tab'],
|
|
75
|
-
'mux.home.task.newline': ['shift+enter'],
|
|
76
|
-
'mux.home.task.status.ready': ['alt+r'],
|
|
77
|
-
'mux.home.task.status.draft': ['alt+d'],
|
|
78
|
-
'mux.home.task.status.complete': ['alt+c'],
|
|
79
|
-
'mux.home.task.reorder.up': ['alt+up'],
|
|
80
|
-
'mux.home.task.reorder.down': ['alt+down'],
|
|
81
|
-
'mux.home.editor.cursor.left': ['left', 'ctrl+b'],
|
|
82
|
-
'mux.home.editor.cursor.right': ['right', 'ctrl+f'],
|
|
83
|
-
'mux.home.editor.cursor.up': ['up'],
|
|
84
|
-
'mux.home.editor.cursor.down': ['down'],
|
|
85
|
-
'mux.home.editor.line.start': ['ctrl+a', 'home'],
|
|
86
|
-
'mux.home.editor.line.end': ['ctrl+e', 'end'],
|
|
87
|
-
'mux.home.editor.word.left': ['alt+b'],
|
|
88
|
-
'mux.home.editor.word.right': ['alt+f'],
|
|
89
|
-
'mux.home.editor.delete.backward': ['backspace'],
|
|
90
|
-
'mux.home.editor.delete.forward': ['delete'],
|
|
91
|
-
'mux.home.editor.delete.word.backward': ['ctrl+w', 'alt+backspace'],
|
|
92
|
-
'mux.home.editor.delete.line.start': ['ctrl+u'],
|
|
93
|
-
'mux.home.editor.delete.line.end': ['ctrl+k'],
|
|
94
|
-
};
|
|
23
|
+
const ACTION_ORDER = TASK_SCREEN_KEYBINDING_ACTION_ORDER;
|
|
95
24
|
|
|
96
25
|
const KEY_TOKEN_ALIASES = new Map<string, string>([
|
|
97
26
|
['cmd', 'meta'],
|
|
@@ -437,10 +366,18 @@ function parseBinding(input: string): ParsedBinding | null {
|
|
|
437
366
|
|
|
438
367
|
function bindingsForAction(raw: readonly string[]): readonly ParsedBinding[] {
|
|
439
368
|
const parsed: ParsedBinding[] = [];
|
|
369
|
+
|
|
370
|
+
const pushIfUnique = (candidate: ParsedBinding): void => {
|
|
371
|
+
if (parsed.some((existing) => strokesEqual(existing.stroke, candidate.stroke))) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
parsed.push(candidate);
|
|
375
|
+
};
|
|
376
|
+
|
|
440
377
|
for (const value of raw) {
|
|
441
378
|
const next = parseBinding(value);
|
|
442
379
|
if (next !== null) {
|
|
443
|
-
|
|
380
|
+
pushIfUnique(next);
|
|
444
381
|
}
|
|
445
382
|
}
|
|
446
383
|
return parsed;
|