parallel-codex-tui 0.1.3 → 0.1.5
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/.parallel-codex/config.example.toml +136 -3
- package/README.md +299 -21
- package/dist/bootstrap.js +37 -17
- package/dist/cli-args.js +34 -3
- package/dist/cli-help.js +20 -0
- package/dist/cli-startup-preflight.js +18 -0
- package/dist/cli-startup-recovery.js +82 -0
- package/dist/cli-workspace-picker.js +330 -0
- package/dist/cli-workspace-transition.js +33 -0
- package/dist/cli-workspace.js +7 -71
- package/dist/cli.js +234 -24
- package/dist/core/collaboration-timeline.js +268 -0
- package/dist/core/config-errors.js +14 -0
- package/dist/core/config.js +297 -109
- package/dist/core/file-store.js +119 -6
- package/dist/core/lease-finalization.js +22 -0
- package/dist/core/paths.js +7 -0
- package/dist/core/process-identity.js +48 -0
- package/dist/core/process-mutation-turn.js +128 -0
- package/dist/core/process-ownership.js +276 -0
- package/dist/core/process-tree.js +90 -0
- package/dist/core/router-audit.js +155 -0
- package/dist/core/router-redaction.js +31 -0
- package/dist/core/router.js +462 -35
- package/dist/core/session-index.js +412 -88
- package/dist/core/session-manager.js +1110 -40
- package/dist/core/task-session-details.js +175 -0
- package/dist/core/task-state-machine.js +18 -0
- package/dist/core/workspace-commit-recovery.js +118 -0
- package/dist/core/workspace.js +19 -11
- package/dist/doctor.js +373 -34
- package/dist/domain/schemas.js +142 -7
- package/dist/orchestrator/collaboration-channel.js +289 -6
- package/dist/orchestrator/feature-plan.js +70 -0
- package/dist/orchestrator/final-acceptance.js +86 -0
- package/dist/orchestrator/judge-artifacts.js +236 -0
- package/dist/orchestrator/orchestrator.js +2086 -203
- package/dist/orchestrator/prompts.js +168 -5
- package/dist/orchestrator/supervisor-summary.js +56 -2
- package/dist/orchestrator/workspace-sandbox.js +927 -0
- package/dist/tui/App.js +3187 -161
- package/dist/tui/AppShell.js +196 -25
- package/dist/tui/CollaborationTimelineView.js +327 -0
- package/dist/tui/FeatureBoardView.js +232 -0
- package/dist/tui/InputBar.js +581 -57
- package/dist/tui/RouterDiagnosticsView.js +469 -0
- package/dist/tui/StatusBar.js +610 -57
- package/dist/tui/StatusDetailView.js +164 -0
- package/dist/tui/TaskSessionDetailView.js +222 -0
- package/dist/tui/TaskSessionsView.js +210 -0
- package/dist/tui/TerminalOutput.js +53 -9
- package/dist/tui/WorkerOutputView.js +1404 -161
- package/dist/tui/WorkerOverviewView.js +269 -0
- package/dist/tui/chat-history.js +25 -0
- package/dist/tui/chat-input.js +67 -19
- package/dist/tui/chat-paste.js +76 -0
- package/dist/tui/display-width.js +41 -3
- package/dist/tui/incremental-text-file.js +101 -0
- package/dist/tui/keyboard.js +49 -0
- package/dist/tui/markdown-text.js +14 -0
- package/dist/tui/raw-input-decoder.js +3 -0
- package/dist/tui/scrolling.js +2 -1
- package/dist/tui/status-line.js +360 -11
- package/dist/tui/task-memory.js +13 -1
- package/dist/tui/task-result.js +105 -0
- package/dist/tui/terminal-screen.js +13 -1
- package/dist/tui/theme-contrast.js +144 -0
- package/dist/tui/theme-preview.js +109 -0
- package/dist/tui/theme.js +158 -0
- package/dist/version.js +1 -1
- package/dist/workers/capabilities.js +213 -0
- package/dist/workers/live-probe.js +177 -0
- package/dist/workers/mock-adapter.js +73 -8
- package/dist/workers/native-attach.js +106 -16
- package/dist/workers/process-adapter.js +572 -77
- package/dist/workers/provider.js +26 -0
- package/dist/workers/registry.js +12 -20
- package/package.json +11 -2
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import { compactEndByDisplayWidth, displayWidth, wrapByDisplayWidth } from "./display-width.js";
|
|
5
|
+
import { TUI_THEME } from "./theme.js";
|
|
6
|
+
export function StatusDetailView({ height = 20, terminalWidth = process.stdout.columns || 120, ...input }) {
|
|
7
|
+
const viewportHeight = Math.max(1, Math.trunc(height));
|
|
8
|
+
const width = Math.max(1, terminalWidth - 2);
|
|
9
|
+
const lines = statusDetailDisplayLines(input, width, viewportHeight);
|
|
10
|
+
const blankRows = Math.max(0, viewportHeight - lines.length);
|
|
11
|
+
return (_jsxs(Box, { flexDirection: "column", height: viewportHeight, children: [lines.map((line, index) => (_jsx(StatusDetailRow, { line: line, width: width }, `${line.tone}-${index}`))), Array.from({ length: blankRows }, (_, index) => (_jsx(Text, { backgroundColor: TUI_THEME.surface, children: " ".repeat(width) }, `status-detail-fill-${index}`)))] }));
|
|
12
|
+
}
|
|
13
|
+
export function statusDetailDisplayLines(input, width, height) {
|
|
14
|
+
const safeWidth = Math.max(1, Math.trunc(width));
|
|
15
|
+
const maxLines = Math.max(1, Math.trunc(height));
|
|
16
|
+
const selected = input.workers[clampWorkerIndex(input.selectedWorkerIndex, input.workers.length)];
|
|
17
|
+
const lines = [
|
|
18
|
+
{ text: "Status", tone: "heading" },
|
|
19
|
+
{
|
|
20
|
+
text: fitStatusDetailText(taskDetail(input), safeWidth),
|
|
21
|
+
tone: input.busy ? "active" : input.canRetry ? "warning" : "text"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
text: fitStatusDetailText(`workspace · ${basename(input.cwd) || input.cwd} · ${input.cwd}`, safeWidth),
|
|
25
|
+
tone: "muted"
|
|
26
|
+
}
|
|
27
|
+
];
|
|
28
|
+
if (input.routeStatus.trim()) {
|
|
29
|
+
lines.push({
|
|
30
|
+
text: fitStatusDetailText(input.routeStatus.trim(), safeWidth),
|
|
31
|
+
tone: /fallback|failed|error|timeout/i.test(input.routeStatus) ? "danger" : "text"
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
lines.push({
|
|
35
|
+
text: fitStatusDetailText(workerSummary(input.workers, input.taskStatus), safeWidth),
|
|
36
|
+
tone: input.workers.some((worker) => worker.runtimeStatus?.state === "failed") ? "danger" : "text"
|
|
37
|
+
});
|
|
38
|
+
if (selected) {
|
|
39
|
+
lines.push(...selectedWorkerLines(selected, safeWidth));
|
|
40
|
+
}
|
|
41
|
+
const reason = sanitizeStatusDetailText(input.routeReason ?? "");
|
|
42
|
+
if (reason) {
|
|
43
|
+
const prefix = "reason · ";
|
|
44
|
+
const wrapped = wrapByDisplayWidth(reason, Math.max(1, safeWidth - displayWidth(prefix))).slice(0, 2);
|
|
45
|
+
wrapped.forEach((part, index) => lines.push({
|
|
46
|
+
text: fitStatusDetailText(`${index === 0 ? prefix : " "}${part}`, safeWidth),
|
|
47
|
+
tone: "muted"
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
return lines.slice(0, maxLines);
|
|
51
|
+
}
|
|
52
|
+
function taskDetail(input) {
|
|
53
|
+
if (!input.taskId) {
|
|
54
|
+
return "task · none";
|
|
55
|
+
}
|
|
56
|
+
const state = input.busy ? "running" : input.canRetry ? "retryable" : "ready";
|
|
57
|
+
return ["task", compactTaskId(input.taskId), input.mode, state].filter(Boolean).join(" · ");
|
|
58
|
+
}
|
|
59
|
+
function workerSummary(workers, taskStatus) {
|
|
60
|
+
if (workers.length === 0) {
|
|
61
|
+
return "workers · none";
|
|
62
|
+
}
|
|
63
|
+
const summary = taskStatus
|
|
64
|
+
.split("|")
|
|
65
|
+
.map((part) => part.trim())
|
|
66
|
+
.filter((part) => /^workers\s+\d+$|^(?:fail|stop|run|wait|done|idle)\s+\d+/i.test(part))
|
|
67
|
+
.join(" · ");
|
|
68
|
+
return summary || `workers · ${workers.length}`;
|
|
69
|
+
}
|
|
70
|
+
function selectedWorkerLines(worker, width) {
|
|
71
|
+
const status = worker.runtimeStatus;
|
|
72
|
+
const state = status?.state ?? "waiting";
|
|
73
|
+
const identity = [
|
|
74
|
+
"selected",
|
|
75
|
+
`${worker.role}/${worker.engine}`,
|
|
76
|
+
state,
|
|
77
|
+
status?.feature_title ?? worker.featureId ?? ""
|
|
78
|
+
].filter(Boolean).join(" · ");
|
|
79
|
+
const model = [status?.model_provider, status?.model_name].filter(Boolean).join("/");
|
|
80
|
+
const phase = status
|
|
81
|
+
? ["phase", humanizeStatusDetail(status.phase), `updated ${formatStatusDetailTime(status.last_event_at)}`].join(" · ")
|
|
82
|
+
: "phase · status pending";
|
|
83
|
+
const lines = [
|
|
84
|
+
{ text: fitStatusDetailText(identity, width), tone: workerStateTone(state) },
|
|
85
|
+
...(model ? [{ text: fitStatusDetailText(`model · ${model}`, width), tone: "muted" }] : []),
|
|
86
|
+
{ text: fitStatusDetailText(phase, width), tone: "muted" }
|
|
87
|
+
];
|
|
88
|
+
if (status?.native_session_id) {
|
|
89
|
+
lines.push({
|
|
90
|
+
text: fitStatusDetailText(`session · ${status.native_session_id}`, width),
|
|
91
|
+
tone: "muted"
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const summary = sanitizeStatusDetailText(status?.summary ?? "");
|
|
95
|
+
if (summary) {
|
|
96
|
+
lines.push({ text: fitStatusDetailText(`summary · ${summary}`, width), tone: "text" });
|
|
97
|
+
}
|
|
98
|
+
return lines;
|
|
99
|
+
}
|
|
100
|
+
function StatusDetailRow({ line, width }) {
|
|
101
|
+
const text = fitStatusDetailText(line.text, width);
|
|
102
|
+
const trailingWidth = Math.max(0, width - displayWidth(text));
|
|
103
|
+
const theme = statusDetailLineTheme(line.tone);
|
|
104
|
+
return (_jsxs(Text, { children: [_jsx(Text, { ...theme, children: text }), trailingWidth > 0 ? _jsx(Text, { backgroundColor: TUI_THEME.surface, children: " ".repeat(trailingWidth) }) : null] }));
|
|
105
|
+
}
|
|
106
|
+
export function statusDetailLineTheme(tone) {
|
|
107
|
+
return {
|
|
108
|
+
backgroundColor: TUI_THEME.surface,
|
|
109
|
+
color: tone === "heading" || tone === "active"
|
|
110
|
+
? TUI_THEME.accent
|
|
111
|
+
: tone === "success"
|
|
112
|
+
? TUI_THEME.success
|
|
113
|
+
: tone === "warning"
|
|
114
|
+
? TUI_THEME.warning
|
|
115
|
+
: tone === "danger"
|
|
116
|
+
? TUI_THEME.danger
|
|
117
|
+
: tone === "muted"
|
|
118
|
+
? TUI_THEME.muted
|
|
119
|
+
: TUI_THEME.text,
|
|
120
|
+
...(tone === "heading" || tone === "danger" ? { bold: true } : {})
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function workerStateTone(state) {
|
|
124
|
+
if (state === "done") {
|
|
125
|
+
return "success";
|
|
126
|
+
}
|
|
127
|
+
if (state === "failed") {
|
|
128
|
+
return "danger";
|
|
129
|
+
}
|
|
130
|
+
if (state === "running" || state === "starting") {
|
|
131
|
+
return "active";
|
|
132
|
+
}
|
|
133
|
+
if (state === "waiting" || state === "cancelled") {
|
|
134
|
+
return "warning";
|
|
135
|
+
}
|
|
136
|
+
return "muted";
|
|
137
|
+
}
|
|
138
|
+
function fitStatusDetailText(text, width) {
|
|
139
|
+
return compactEndByDisplayWidth(sanitizeStatusDetailText(text), Math.max(1, width));
|
|
140
|
+
}
|
|
141
|
+
function sanitizeStatusDetailText(text) {
|
|
142
|
+
return text
|
|
143
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
144
|
+
.replace(/[\u0000-\u001f\u007f]/g, " ")
|
|
145
|
+
.replace(/\s+/g, " ")
|
|
146
|
+
.trim();
|
|
147
|
+
}
|
|
148
|
+
function humanizeStatusDetail(value) {
|
|
149
|
+
return sanitizeStatusDetailText(value).replace(/[-_]+/g, " ");
|
|
150
|
+
}
|
|
151
|
+
function formatStatusDetailTime(value) {
|
|
152
|
+
const parsed = new Date(value);
|
|
153
|
+
if (!Number.isFinite(parsed.getTime())) {
|
|
154
|
+
return "unknown";
|
|
155
|
+
}
|
|
156
|
+
return parsed.toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "Z");
|
|
157
|
+
}
|
|
158
|
+
function compactTaskId(taskId) {
|
|
159
|
+
const withoutPrefix = taskId.startsWith("task-") ? taskId.slice("task-".length) : taskId;
|
|
160
|
+
return withoutPrefix.replace(/^\d{8}-/, "");
|
|
161
|
+
}
|
|
162
|
+
function clampWorkerIndex(index, count) {
|
|
163
|
+
return Math.min(Math.max(0, count - 1), Math.max(0, Math.trunc(index)));
|
|
164
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { compactEndByDisplayWidth, displayWidth } from "./display-width.js";
|
|
4
|
+
import { TUI_THEME } from "./theme.js";
|
|
5
|
+
export function TaskSessionDetailView({ details, selectedWorkerIndex, loading = false, error = null, notice = null, height = 20, terminalWidth = process.stdout.columns || 120 }) {
|
|
6
|
+
const viewportHeight = Math.max(1, Math.trunc(height));
|
|
7
|
+
const width = taskSessionDetailContentWidth(terminalWidth);
|
|
8
|
+
const lines = taskSessionDetailDisplayLines(details, selectedWorkerIndex, viewportHeight, terminalWidth, {
|
|
9
|
+
loading,
|
|
10
|
+
error,
|
|
11
|
+
notice
|
|
12
|
+
});
|
|
13
|
+
const blankRows = Math.max(0, viewportHeight - lines.length);
|
|
14
|
+
return (_jsxs(Box, { flexDirection: "column", height: viewportHeight, children: [lines.map((line, index) => (_jsx(TaskSessionDetailRow, { line: line, width: width }, `${line.kind}-${line.workerIndex ?? index}-${index}`))), Array.from({ length: blankRows }, (_, index) => (_jsx(Text, { backgroundColor: TUI_THEME.surface, children: " ".repeat(width) }, `task-session-detail-fill-${index}`)))] }));
|
|
15
|
+
}
|
|
16
|
+
export function taskSessionDetailDisplayLines(details, selectedWorkerIndex, height, terminalWidth, state = {}) {
|
|
17
|
+
const viewportHeight = Math.max(1, Math.trunc(height));
|
|
18
|
+
const width = taskSessionDetailContentWidth(terminalWidth);
|
|
19
|
+
const header = [
|
|
20
|
+
{ text: fitDetailCandidates(["Session hierarchy", "Session detail", "Session", "S"], width), tone: "heading", kind: "heading" }
|
|
21
|
+
];
|
|
22
|
+
if (details && viewportHeight >= 3) {
|
|
23
|
+
header.push({
|
|
24
|
+
text: fitDetailCandidates([
|
|
25
|
+
`project · ${safeDetailText(details.projectName)} · ${safeDetailText(details.projectPath)}`,
|
|
26
|
+
`project · ${safeDetailText(details.projectName)}`,
|
|
27
|
+
safeDetailText(details.projectName)
|
|
28
|
+
], width),
|
|
29
|
+
tone: "muted",
|
|
30
|
+
kind: "project"
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (details && viewportHeight >= 4) {
|
|
34
|
+
header.push({
|
|
35
|
+
text: fitDetailCandidates([
|
|
36
|
+
`task · ${safeDetailText(details.task.title)} · ${humanizeDetailState(details.task.status)} · ${details.turns.length} turns · ${details.workers.length} workers`,
|
|
37
|
+
`task · ${safeDetailText(details.task.title)} · ${humanizeDetailState(details.task.status)}`,
|
|
38
|
+
safeDetailText(details.task.title)
|
|
39
|
+
], width),
|
|
40
|
+
tone: detailTaskTone(details.task.status),
|
|
41
|
+
kind: "task"
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (state.notice && header.length < viewportHeight) {
|
|
45
|
+
header.push({ text: fitDetailText(safeDetailText(state.notice), width), tone: "active", kind: "heading" });
|
|
46
|
+
}
|
|
47
|
+
const bodySlots = Math.max(0, viewportHeight - header.length);
|
|
48
|
+
if (bodySlots === 0) {
|
|
49
|
+
return header;
|
|
50
|
+
}
|
|
51
|
+
if (state.loading) {
|
|
52
|
+
return [...header, { text: fitDetailText("loading session hierarchy", width), tone: "muted", kind: "empty" }];
|
|
53
|
+
}
|
|
54
|
+
if (state.error) {
|
|
55
|
+
return [...header, {
|
|
56
|
+
text: fitDetailText(`error · ${safeDetailText(state.error)}`, width),
|
|
57
|
+
tone: "danger",
|
|
58
|
+
kind: "error"
|
|
59
|
+
}];
|
|
60
|
+
}
|
|
61
|
+
if (!details) {
|
|
62
|
+
return [...header, { text: fitDetailText("No task selected", width), tone: "muted", kind: "empty" }];
|
|
63
|
+
}
|
|
64
|
+
if (details.turns.length === 0) {
|
|
65
|
+
return [...header, { text: fitDetailText("No persisted turns", width), tone: "muted", kind: "empty" }];
|
|
66
|
+
}
|
|
67
|
+
const selected = clampDetailWorkerIndex(selectedWorkerIndex, details.workers.length);
|
|
68
|
+
const body = taskSessionDetailBodyLines(details, selected, width);
|
|
69
|
+
const selectedLine = Math.max(0, body.findIndex((line) => line.workerIndex === selected && line.kind === "worker"));
|
|
70
|
+
let start = Math.min(Math.max(0, body.length - bodySlots), Math.max(0, selectedLine - Math.floor(bodySlots / 2)));
|
|
71
|
+
if (start > 0) {
|
|
72
|
+
for (let index = selectedLine; index >= start; index -= 1) {
|
|
73
|
+
if (body[index]?.kind === "turn" && selectedLine - index < bodySlots) {
|
|
74
|
+
start = index;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return [...header, ...body.slice(start, start + bodySlots)];
|
|
80
|
+
}
|
|
81
|
+
export function moveTaskSessionDetailSelection(current, delta, workerCount, wrap = false) {
|
|
82
|
+
if (workerCount <= 0) {
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
const normalized = clampDetailWorkerIndex(current, workerCount);
|
|
86
|
+
const next = normalized + Math.trunc(delta);
|
|
87
|
+
if (wrap) {
|
|
88
|
+
return ((next % workerCount) + workerCount) % workerCount;
|
|
89
|
+
}
|
|
90
|
+
return Math.min(workerCount - 1, Math.max(0, next));
|
|
91
|
+
}
|
|
92
|
+
function taskSessionDetailBodyLines(details, selectedWorkerIndex, width) {
|
|
93
|
+
const workerIndexById = new Map(details.workers.map((worker, index) => [worker.id, index]));
|
|
94
|
+
return details.turns.flatMap((turn) => {
|
|
95
|
+
const turnNumber = Number(turn.turnId);
|
|
96
|
+
const turnLabel = Number.isInteger(turnNumber) ? `Turn ${turnNumber}` : `Turn ${turn.turnId}`;
|
|
97
|
+
const lines = [{
|
|
98
|
+
text: fitDetailCandidates([
|
|
99
|
+
`${turnLabel} · ${formatDetailTime(turn.createdAt)} · ${safeDetailText(turn.request) || "request unavailable"}`,
|
|
100
|
+
`${turnLabel} · ${safeDetailText(turn.request) || "request unavailable"}`,
|
|
101
|
+
turnLabel
|
|
102
|
+
], width),
|
|
103
|
+
tone: "active",
|
|
104
|
+
kind: "turn"
|
|
105
|
+
}];
|
|
106
|
+
if (turn.workers.length === 0) {
|
|
107
|
+
lines.push({ text: fitDetailText(" no workers", width), tone: "muted", kind: "empty" });
|
|
108
|
+
return lines;
|
|
109
|
+
}
|
|
110
|
+
for (const worker of turn.workers) {
|
|
111
|
+
const workerIndex = workerIndexById.get(worker.id) ?? 0;
|
|
112
|
+
lines.push({
|
|
113
|
+
text: detailWorkerText(worker, workerIndex === selectedWorkerIndex, width),
|
|
114
|
+
tone: detailWorkerTone(worker),
|
|
115
|
+
workerIndex,
|
|
116
|
+
kind: "worker"
|
|
117
|
+
});
|
|
118
|
+
lines.push({
|
|
119
|
+
text: detailNativeText(worker, width),
|
|
120
|
+
tone: "muted",
|
|
121
|
+
workerIndex,
|
|
122
|
+
kind: "native"
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return lines;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function detailWorkerText(worker, selected, width) {
|
|
129
|
+
const marker = selected ? "> " : " ";
|
|
130
|
+
const role = `${worker.role.slice(0, 1).toUpperCase()}${worker.role.slice(1)}`;
|
|
131
|
+
const engine = [worker.engine, worker.model, worker.modelProvider].filter(Boolean).join("/");
|
|
132
|
+
const feature = worker.featureTitle ?? worker.featureId ?? "";
|
|
133
|
+
return fitDetailCandidates([
|
|
134
|
+
`${marker}${role} · ${engine} · ${feature ? `${safeDetailText(feature)} · ` : ""}${worker.state} · ${formatDetailTime(worker.lastActivityAt)}`,
|
|
135
|
+
`${marker}${role} · ${engine} · ${worker.state}`,
|
|
136
|
+
`${marker}${role} · ${worker.engine}`,
|
|
137
|
+
`${marker}${safeDetailText(worker.id)}`,
|
|
138
|
+
marker.trimEnd()
|
|
139
|
+
], width);
|
|
140
|
+
}
|
|
141
|
+
function detailNativeText(worker, width) {
|
|
142
|
+
const session = worker.nativeSession;
|
|
143
|
+
const cwd = safeDetailText(session?.cwd ?? worker.dir);
|
|
144
|
+
if (!session) {
|
|
145
|
+
return fitDetailCandidates([
|
|
146
|
+
` native · none · cwd ${cwd}`,
|
|
147
|
+
` native · none`,
|
|
148
|
+
" no native session"
|
|
149
|
+
], width);
|
|
150
|
+
}
|
|
151
|
+
return fitDetailCandidates([
|
|
152
|
+
` native · ${safeDetailText(session.sessionId)} · cwd ${cwd} · used ${formatDetailTime(session.lastUsedAt)}`,
|
|
153
|
+
` native · ${safeDetailText(session.sessionId)} · used ${formatDetailTime(session.lastUsedAt)}`,
|
|
154
|
+
` native · ${safeDetailText(session.sessionId)}`,
|
|
155
|
+
` session ${safeDetailText(session.sessionId)}`
|
|
156
|
+
], width);
|
|
157
|
+
}
|
|
158
|
+
function TaskSessionDetailRow({ line, width }) {
|
|
159
|
+
const fill = Math.max(0, width - displayWidth(line.text));
|
|
160
|
+
return (_jsxs(Text, { children: [_jsx(Text, { ...detailLineTheme(line.tone), children: line.text }), fill > 0 ? _jsx(Text, { backgroundColor: TUI_THEME.surface, children: " ".repeat(fill) }) : null] }));
|
|
161
|
+
}
|
|
162
|
+
function detailTaskTone(status) {
|
|
163
|
+
if (status === "done")
|
|
164
|
+
return "success";
|
|
165
|
+
if (status === "failed")
|
|
166
|
+
return "danger";
|
|
167
|
+
if (status === "paused" || status === "cancelled")
|
|
168
|
+
return "warning";
|
|
169
|
+
return "active";
|
|
170
|
+
}
|
|
171
|
+
function detailWorkerTone(worker) {
|
|
172
|
+
if (worker.state === "done")
|
|
173
|
+
return "success";
|
|
174
|
+
if (worker.state === "failed")
|
|
175
|
+
return "danger";
|
|
176
|
+
if (worker.state === "cancelled")
|
|
177
|
+
return "warning";
|
|
178
|
+
if (worker.state === "running" || worker.state === "starting")
|
|
179
|
+
return "active";
|
|
180
|
+
return "muted";
|
|
181
|
+
}
|
|
182
|
+
function detailLineTheme(tone) {
|
|
183
|
+
return {
|
|
184
|
+
backgroundColor: TUI_THEME.surface,
|
|
185
|
+
color: tone === "heading" || tone === "active"
|
|
186
|
+
? TUI_THEME.accent
|
|
187
|
+
: tone === "success"
|
|
188
|
+
? TUI_THEME.success
|
|
189
|
+
: tone === "warning"
|
|
190
|
+
? TUI_THEME.warning
|
|
191
|
+
: tone === "danger"
|
|
192
|
+
? TUI_THEME.danger
|
|
193
|
+
: TUI_THEME.muted,
|
|
194
|
+
...(tone === "heading" || tone === "danger" ? { bold: true } : {})
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function clampDetailWorkerIndex(index, count) {
|
|
198
|
+
return Math.min(Math.max(0, count - 1), Math.max(0, Math.trunc(index)));
|
|
199
|
+
}
|
|
200
|
+
function humanizeDetailState(value) {
|
|
201
|
+
return value.replaceAll("_", " ");
|
|
202
|
+
}
|
|
203
|
+
function formatDetailTime(value) {
|
|
204
|
+
return value.slice(5, 16).replace("T", " ");
|
|
205
|
+
}
|
|
206
|
+
function fitDetailCandidates(candidates, width) {
|
|
207
|
+
return candidates.find((candidate) => displayWidth(candidate) <= width)
|
|
208
|
+
?? fitDetailText(candidates.at(-1) ?? "", width);
|
|
209
|
+
}
|
|
210
|
+
function fitDetailText(text, width) {
|
|
211
|
+
return compactEndByDisplayWidth(text, Math.max(1, width));
|
|
212
|
+
}
|
|
213
|
+
function safeDetailText(text) {
|
|
214
|
+
return text
|
|
215
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
216
|
+
.replace(/[\u0000-\u001f\u007f]/g, " ")
|
|
217
|
+
.replace(/\s+/g, " ")
|
|
218
|
+
.trim();
|
|
219
|
+
}
|
|
220
|
+
function taskSessionDetailContentWidth(terminalWidth) {
|
|
221
|
+
return Math.max(1, terminalWidth - 2);
|
|
222
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { compactEndByDisplayWidth, displayWidth } from "./display-width.js";
|
|
4
|
+
import { TUI_THEME } from "./theme.js";
|
|
5
|
+
export function TaskSessionsView({ tasks, activeTaskId, selectedIndex, includeArchived = false, notice = null, action = null, loading = false, error = null, height = 20, terminalWidth = process.stdout.columns || 120 }) {
|
|
6
|
+
const viewportHeight = Math.max(1, height);
|
|
7
|
+
const width = taskSessionsContentWidth(terminalWidth);
|
|
8
|
+
const lines = taskSessionsDisplayLines(tasks, activeTaskId, selectedIndex, viewportHeight, terminalWidth, {
|
|
9
|
+
loading,
|
|
10
|
+
error,
|
|
11
|
+
includeArchived,
|
|
12
|
+
notice,
|
|
13
|
+
action
|
|
14
|
+
});
|
|
15
|
+
const blankRows = Math.max(0, viewportHeight - lines.length);
|
|
16
|
+
return (_jsxs(Box, { flexDirection: "column", height: viewportHeight, children: [lines.map((line, index) => (_jsx(TaskSessionRow, { line: line, width: width }, `${line.taskIndex ?? line.tone}-${index}`))), Array.from({ length: blankRows }, (_, index) => (_jsx(Text, { backgroundColor: TUI_THEME.surface, children: " ".repeat(width) }, `task-session-fill-${index}`)))] }));
|
|
17
|
+
}
|
|
18
|
+
export function taskSessionsDisplayLines(tasks, activeTaskId, selectedIndex, height, terminalWidth, state = {}) {
|
|
19
|
+
const viewportHeight = Math.max(1, Math.trunc(height));
|
|
20
|
+
const width = taskSessionsContentWidth(terminalWidth);
|
|
21
|
+
const lines = [
|
|
22
|
+
{
|
|
23
|
+
text: fitTaskSessionCandidates([
|
|
24
|
+
state.includeArchived ? "Task sessions · archived shown" : "Task sessions",
|
|
25
|
+
state.includeArchived ? "Sessions · all" : "Sessions",
|
|
26
|
+
"Tasks",
|
|
27
|
+
"T"
|
|
28
|
+
], width),
|
|
29
|
+
tone: "heading"
|
|
30
|
+
}
|
|
31
|
+
];
|
|
32
|
+
if (viewportHeight >= 3) {
|
|
33
|
+
lines.push({ text: taskSessionSummary(tasks, width), tone: "muted" });
|
|
34
|
+
}
|
|
35
|
+
if (viewportHeight >= 4 && state.action) {
|
|
36
|
+
lines.push({
|
|
37
|
+
text: fitTaskSessionText(state.action.type === "rename"
|
|
38
|
+
? `rename · ${safeTaskSessionText(state.action.title)} · Enter save · Esc cancel`
|
|
39
|
+
: `delete · ${safeTaskSessionText(state.action.title)} · press D again · Esc cancel`, width),
|
|
40
|
+
tone: state.action.type === "delete" ? "danger" : "active"
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
else if (viewportHeight >= 4 && state.notice) {
|
|
44
|
+
lines.push({ text: fitTaskSessionText(state.notice, width), tone: "success" });
|
|
45
|
+
}
|
|
46
|
+
const slots = Math.max(0, viewportHeight - lines.length);
|
|
47
|
+
if (state.loading) {
|
|
48
|
+
if (slots > 0) {
|
|
49
|
+
lines.push({ text: fitTaskSessionText("loading task sessions", width), tone: "muted" });
|
|
50
|
+
}
|
|
51
|
+
return lines;
|
|
52
|
+
}
|
|
53
|
+
if (state.error) {
|
|
54
|
+
if (slots > 0) {
|
|
55
|
+
lines.push({ text: fitTaskSessionText(`error · ${safeTaskSessionText(state.error)}`, width), tone: "danger" });
|
|
56
|
+
}
|
|
57
|
+
return lines;
|
|
58
|
+
}
|
|
59
|
+
if (tasks.length === 0) {
|
|
60
|
+
if (slots > 0) {
|
|
61
|
+
lines.push({ text: fitTaskSessionText("No saved task sessions", width), tone: "muted" });
|
|
62
|
+
}
|
|
63
|
+
return lines;
|
|
64
|
+
}
|
|
65
|
+
const selected = clampTaskIndex(selectedIndex, tasks.length);
|
|
66
|
+
const visibleCount = Math.min(slots, tasks.length);
|
|
67
|
+
const start = taskSessionWindowStart(selected, tasks.length, visibleCount);
|
|
68
|
+
for (let index = start; index < start + visibleCount; index += 1) {
|
|
69
|
+
const task = tasks[index];
|
|
70
|
+
if (!task) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
lines.push({
|
|
74
|
+
text: taskSessionRowText(task, index === selected, task.id === activeTaskId, width),
|
|
75
|
+
tone: taskSessionStatusTone(task),
|
|
76
|
+
taskIndex: index
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return lines;
|
|
80
|
+
}
|
|
81
|
+
export function moveTaskSessionSelection(current, delta, taskCount, wrap = false) {
|
|
82
|
+
if (taskCount <= 0) {
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
const normalizedCurrent = clampTaskIndex(current, taskCount);
|
|
86
|
+
const next = normalizedCurrent + Math.trunc(delta);
|
|
87
|
+
if (wrap) {
|
|
88
|
+
return ((next % taskCount) + taskCount) % taskCount;
|
|
89
|
+
}
|
|
90
|
+
return Math.min(taskCount - 1, Math.max(0, next));
|
|
91
|
+
}
|
|
92
|
+
function TaskSessionRow({ line, width }) {
|
|
93
|
+
const trailingWidth = Math.max(0, width - displayWidth(line.text));
|
|
94
|
+
const theme = taskSessionLineTheme(line.tone);
|
|
95
|
+
return (_jsxs(Text, { children: [_jsx(Text, { ...theme, children: line.text }), trailingWidth > 0 ? _jsx(Text, { backgroundColor: TUI_THEME.surface, children: " ".repeat(trailingWidth) }) : null] }));
|
|
96
|
+
}
|
|
97
|
+
function taskSessionSummary(tasks, width) {
|
|
98
|
+
const counts = new Map();
|
|
99
|
+
let archived = 0;
|
|
100
|
+
for (const task of tasks) {
|
|
101
|
+
if (task.archived_at) {
|
|
102
|
+
archived += 1;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const group = taskSessionStatusGroup(task.status);
|
|
106
|
+
counts.set(group, (counts.get(group) ?? 0) + 1);
|
|
107
|
+
}
|
|
108
|
+
const parts = ["running", "paused", "done", "failed", "cancelled"].flatMap((group) => {
|
|
109
|
+
const count = counts.get(group) ?? 0;
|
|
110
|
+
return count > 0 ? [`${count} ${group}`] : [];
|
|
111
|
+
});
|
|
112
|
+
if (archived > 0) {
|
|
113
|
+
parts.push(`${archived} archived`);
|
|
114
|
+
}
|
|
115
|
+
return fitTaskSessionCandidates([
|
|
116
|
+
[`${tasks.length} ${tasks.length === 1 ? "task" : "tasks"}`, ...parts].join(" · "),
|
|
117
|
+
`${tasks.length} tasks · ${counts.get("running") ?? 0} active · ${counts.get("failed") ?? 0} failed`,
|
|
118
|
+
`${tasks.length} tasks`,
|
|
119
|
+
`${tasks.length}t`
|
|
120
|
+
], width);
|
|
121
|
+
}
|
|
122
|
+
function taskSessionRowText(task, selected, active, width) {
|
|
123
|
+
const marker = `${selected ? ">" : " "} ${active ? "*" : " "} `;
|
|
124
|
+
const title = safeTaskSessionText(task.title);
|
|
125
|
+
const status = task.archived_at
|
|
126
|
+
? `archived · ${humanizeTaskSessionStatus(task.status)}`
|
|
127
|
+
: humanizeTaskSessionStatus(task.status);
|
|
128
|
+
const date = task.created_at.slice(5, 16).replace("T", " ");
|
|
129
|
+
const turns = `${task.turnCount} ${task.turnCount === 1 ? "turn" : "turns"}`;
|
|
130
|
+
const workers = `${task.workerCount} ${task.workerCount === 1 ? "worker" : "workers"}`;
|
|
131
|
+
const native = `${task.nativeSessionCount} native`;
|
|
132
|
+
const compactId = task.id.replace(/^task-/, "#");
|
|
133
|
+
return fitTaskSessionCandidates([
|
|
134
|
+
[marker + title, status, turns, workers, native, date].join(" · "),
|
|
135
|
+
[marker + title, status, turns, workers, native].join(" · "),
|
|
136
|
+
[marker + title, status, turns, workers].join(" · "),
|
|
137
|
+
[marker + title, status].join(" · "),
|
|
138
|
+
[marker + compactId, status].join(" · "),
|
|
139
|
+
marker.trimEnd()
|
|
140
|
+
], width);
|
|
141
|
+
}
|
|
142
|
+
function taskSessionWindowStart(selected, count, visibleCount) {
|
|
143
|
+
if (visibleCount <= 0 || count <= visibleCount) {
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
return Math.min(count - visibleCount, Math.max(0, selected - Math.floor(visibleCount / 2)));
|
|
147
|
+
}
|
|
148
|
+
function clampTaskIndex(index, count) {
|
|
149
|
+
return Math.min(Math.max(0, count - 1), Math.max(0, Math.trunc(index)));
|
|
150
|
+
}
|
|
151
|
+
function taskSessionStatusGroup(status) {
|
|
152
|
+
if (status === "paused" || status === "done" || status === "failed" || status === "cancelled") {
|
|
153
|
+
return status;
|
|
154
|
+
}
|
|
155
|
+
return "running";
|
|
156
|
+
}
|
|
157
|
+
function humanizeTaskSessionStatus(status) {
|
|
158
|
+
return status.replace(/_/g, " ");
|
|
159
|
+
}
|
|
160
|
+
function taskSessionStatusTone(task) {
|
|
161
|
+
if (task.archived_at) {
|
|
162
|
+
return "muted";
|
|
163
|
+
}
|
|
164
|
+
const status = task.status;
|
|
165
|
+
if (status === "done") {
|
|
166
|
+
return "success";
|
|
167
|
+
}
|
|
168
|
+
if (status === "paused") {
|
|
169
|
+
return "warning";
|
|
170
|
+
}
|
|
171
|
+
if (status === "failed") {
|
|
172
|
+
return "danger";
|
|
173
|
+
}
|
|
174
|
+
if (status === "cancelled") {
|
|
175
|
+
return "warning";
|
|
176
|
+
}
|
|
177
|
+
return "active";
|
|
178
|
+
}
|
|
179
|
+
function fitTaskSessionCandidates(candidates, width) {
|
|
180
|
+
return candidates.find((candidate) => displayWidth(candidate) <= width)
|
|
181
|
+
?? fitTaskSessionText(candidates.at(-1) ?? "", width);
|
|
182
|
+
}
|
|
183
|
+
function fitTaskSessionText(text, width) {
|
|
184
|
+
return compactEndByDisplayWidth(text, Math.max(1, width));
|
|
185
|
+
}
|
|
186
|
+
function safeTaskSessionText(text) {
|
|
187
|
+
return text
|
|
188
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
189
|
+
.replace(/[\u0000-\u001f\u007f]/g, " ")
|
|
190
|
+
.replace(/\s+/g, " ")
|
|
191
|
+
.trim();
|
|
192
|
+
}
|
|
193
|
+
function taskSessionLineTheme(tone) {
|
|
194
|
+
return {
|
|
195
|
+
backgroundColor: TUI_THEME.surface,
|
|
196
|
+
color: tone === "heading" || tone === "active"
|
|
197
|
+
? TUI_THEME.accent
|
|
198
|
+
: tone === "success"
|
|
199
|
+
? TUI_THEME.success
|
|
200
|
+
: tone === "warning"
|
|
201
|
+
? TUI_THEME.warning
|
|
202
|
+
: tone === "danger"
|
|
203
|
+
? TUI_THEME.danger
|
|
204
|
+
: TUI_THEME.muted,
|
|
205
|
+
...(tone === "heading" || tone === "danger" ? { bold: true } : {})
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function taskSessionsContentWidth(terminalWidth) {
|
|
209
|
+
return Math.max(1, terminalWidth - 2);
|
|
210
|
+
}
|
|
@@ -1,18 +1,36 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
|
|
3
|
+
import { displayWidth } from "./display-width.js";
|
|
4
|
+
import { TUI_THEME } from "./theme.js";
|
|
5
|
+
const TERMINAL_OUTPUT_EMPTY_TEXT = "waiting for output";
|
|
6
|
+
export function TerminalOutput({ lines, minLines, width }) {
|
|
7
|
+
const blankTailLineCount = terminalOutputBlankTailLineCount(lines.length === 0 ? 1 : lines.length, minLines);
|
|
4
8
|
if (lines.length === 0) {
|
|
5
|
-
return _jsx(Text, { children:
|
|
9
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { ...terminalOutputEmptyTheme(), children: TERMINAL_OUTPUT_EMPTY_TEXT }), _jsx(TerminalOutputEmptyTrailingFill, { width: width })] }), _jsx(TerminalOutputBlankTailLines, { count: blankTailLineCount, width: width })] }));
|
|
6
10
|
}
|
|
7
|
-
return (
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
return (_jsxs(Box, { flexDirection: "column", children: [lines.map((line, index) => (_jsx(Text, { children: line.chunks.length === 0
|
|
12
|
+
? _jsx(Text, { ...terminalOutputBlankLineTheme(), children: " ".repeat(terminalOutputBlankLineWidth(width)) })
|
|
13
|
+
: _jsxs(_Fragment, { children: [line.chunks.map((chunk, chunkIndex) => (_jsx(Text, { ...terminalOutputTextProps(chunk.style), children: chunk.text }, chunkIndex))), _jsx(TerminalOutputTrailingFill, { line: line, width: width })] }) }, index))), _jsx(TerminalOutputBlankTailLines, { count: blankTailLineCount, width: width })] }));
|
|
10
14
|
}
|
|
11
|
-
function
|
|
15
|
+
export function terminalOutputEmptyTheme() {
|
|
12
16
|
return {
|
|
13
|
-
backgroundColor:
|
|
17
|
+
backgroundColor: TUI_THEME.surface,
|
|
18
|
+
color: TUI_THEME.muted
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function terminalOutputEmptyText() {
|
|
22
|
+
return TERMINAL_OUTPUT_EMPTY_TEXT;
|
|
23
|
+
}
|
|
24
|
+
export function terminalOutputBlankLineTheme() {
|
|
25
|
+
return {
|
|
26
|
+
backgroundColor: TUI_THEME.surface
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function terminalOutputTextProps(style) {
|
|
30
|
+
return {
|
|
31
|
+
backgroundColor: style.backgroundColor ?? TUI_THEME.surface,
|
|
14
32
|
bold: style.bold,
|
|
15
|
-
color: style.color,
|
|
33
|
+
color: style.color ?? TUI_THEME.text,
|
|
16
34
|
dimColor: style.dimColor,
|
|
17
35
|
inverse: style.inverse || style.cursor,
|
|
18
36
|
italic: style.italic,
|
|
@@ -20,3 +38,29 @@ function textProps(style) {
|
|
|
20
38
|
underline: style.underline
|
|
21
39
|
};
|
|
22
40
|
}
|
|
41
|
+
export function terminalOutputTrailingFillWidth(line, width) {
|
|
42
|
+
if (width === undefined) {
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
return Math.max(0, width - terminalOutputLineDisplayWidth(line));
|
|
46
|
+
}
|
|
47
|
+
function TerminalOutputTrailingFill({ line, width }) {
|
|
48
|
+
const fillWidth = terminalOutputTrailingFillWidth(line, width);
|
|
49
|
+
return fillWidth > 0 ? _jsx(Text, { ...terminalOutputBlankLineTheme(), children: " ".repeat(fillWidth) }) : null;
|
|
50
|
+
}
|
|
51
|
+
function TerminalOutputEmptyTrailingFill({ width }) {
|
|
52
|
+
const fillWidth = width === undefined ? 0 : Math.max(0, width - displayWidth(TERMINAL_OUTPUT_EMPTY_TEXT));
|
|
53
|
+
return fillWidth > 0 ? _jsx(Text, { ...terminalOutputBlankLineTheme(), children: " ".repeat(fillWidth) }) : null;
|
|
54
|
+
}
|
|
55
|
+
function TerminalOutputBlankTailLines({ count, width }) {
|
|
56
|
+
return (_jsx(_Fragment, { children: Array.from({ length: count }, (_, index) => (_jsx(Text, { ...terminalOutputBlankLineTheme(), children: " ".repeat(terminalOutputBlankLineWidth(width)) }, `blank-tail-${index}`))) }));
|
|
57
|
+
}
|
|
58
|
+
function terminalOutputBlankTailLineCount(lineCount, minLines) {
|
|
59
|
+
return minLines === undefined ? 0 : Math.max(0, minLines - lineCount);
|
|
60
|
+
}
|
|
61
|
+
function terminalOutputLineDisplayWidth(line) {
|
|
62
|
+
return line.chunks.reduce((sum, chunk) => sum + displayWidth(chunk.text), 0);
|
|
63
|
+
}
|
|
64
|
+
function terminalOutputBlankLineWidth(width) {
|
|
65
|
+
return Math.max(1, width ?? 1);
|
|
66
|
+
}
|