@tt-a1i/openpi 0.3.1 → 0.5.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/README.md +184 -59
- package/SETUP.md +23 -7
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +145 -0
- package/extensions/ask-user/index.ts +30 -14
- package/extensions/background-terminals/index.ts +30 -2
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +486 -106
- package/extensions/background-terminals/src/output.ts +33 -0
- package/extensions/background-terminals/src/prompt.ts +14 -6
- package/extensions/background-terminals/src/result-delivery.ts +4 -1
- package/extensions/background-terminals/src/ui/ps.ts +132 -129
- package/extensions/capabilities/index.ts +30 -42
- package/extensions/capabilities/src/ui.ts +93 -0
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/schedule.ts +7 -1
- package/extensions/file-mutation-display/index.ts +34 -76
- package/extensions/file-mutation-display/render.ts +146 -87
- package/extensions/file-search/index.ts +8 -7
- package/extensions/file-search/src/binaries.ts +75 -59
- package/extensions/git-info/src/changed-files-view.ts +47 -14
- package/extensions/git-read/index.ts +328 -0
- package/extensions/git-read/src/args.ts +171 -0
- package/extensions/git-read/src/process.ts +81 -0
- package/extensions/git-read/src/prompt.ts +56 -0
- package/extensions/model-info/index.ts +21 -33
- package/extensions/model-info/session-metrics.ts +96 -0
- package/extensions/plan-mode/bash-policy.ts +54 -9
- package/extensions/plan-mode/index.ts +7 -2
- package/extensions/post-edit/index.ts +16 -6
- package/extensions/sessions/git-stats.ts +258 -72
- package/extensions/sessions/index.ts +222 -140
- package/extensions/sessions/preview-cache.ts +104 -0
- package/extensions/sessions/preview-loader.ts +856 -0
- package/extensions/sessions/sessions.ts +43 -4
- package/extensions/setup/index.ts +127 -131
- package/extensions/shared/activity-status.ts +36 -5
- package/extensions/shared/agent-session-page.ts +319 -0
- package/extensions/shared/agent-tool-renderer.ts +218 -0
- package/extensions/shared/agent-transcript.ts +524 -0
- package/extensions/shared/below-editor-navigation.ts +26 -0
- package/extensions/shared/capability-intent.ts +53 -0
- package/extensions/shared/child-session.ts +444 -22
- package/extensions/shared/result-budget.ts +134 -0
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/screen-chrome.ts +133 -0
- package/extensions/shared/setup-config.ts +97 -38
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/spinner.ts +28 -0
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +113 -0
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +42 -8
- package/extensions/shared/transcript-viewport.ts +46 -0
- package/extensions/shared/web-observer-registry.ts +390 -0
- package/extensions/shared/worktree.ts +11 -0
- package/extensions/subagents/index.ts +461 -186
- package/extensions/subagents/navigation.ts +86 -28
- package/extensions/subagents/src/agent-types.ts +37 -15
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +375 -66
- package/extensions/subagents/src/domain.ts +5 -0
- package/extensions/subagents/src/id-sequence.ts +84 -0
- package/extensions/subagents/src/manager.ts +651 -536
- package/extensions/subagents/src/prompt.ts +185 -42
- package/extensions/subagents/src/result-artifact.ts +146 -0
- package/extensions/subagents/src/result-delivery.ts +7 -1
- package/extensions/subagents/src/runtime.ts +23 -6
- package/extensions/subagents/src/ui/takeover.ts +128 -337
- package/extensions/subagents/src/ui/transcript.ts +38 -501
- package/extensions/subagents/src/ui/wait-result.ts +103 -15
- package/extensions/suggestions/src/ui.ts +10 -4
- package/extensions/tasks/index.ts +0 -3
- package/extensions/tasks/ui.ts +79 -62
- package/extensions/ui-customization/footer.ts +7 -44
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +185 -0
- package/extensions/web/index.ts +234 -0
- package/extensions/workflows/artifacts.ts +147 -22
- package/extensions/workflows/completion-projection.ts +457 -0
- package/extensions/workflows/controller.ts +14 -2
- package/extensions/workflows/coordinator.ts +62 -0
- package/extensions/workflows/dashboard.ts +458 -339
- package/extensions/workflows/handoff.ts +121 -25
- package/extensions/workflows/index.ts +1042 -492
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +131 -19
- package/extensions/workflows/navigation.ts +61 -18
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +166 -10
- package/extensions/workflows/replay-safety.ts +58 -27
- package/extensions/workflows/result-delivery.ts +253 -0
- package/extensions/workflows/retention.ts +593 -0
- package/extensions/workflows/runner.ts +388 -279
- package/extensions/workflows/sandbox-child.cjs +36 -3
- package/extensions/workflows/sandbox.ts +62 -8
- package/extensions/workflows/serialization.ts +325 -17
- package/extensions/workflows/tool-renderer.ts +22 -0
- package/extensions/workflows/transcript.ts +149 -0
- package/extensions/workspace-cleanup-guard/index.ts +54 -0
- package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
- package/package.json +28 -8
- package/skills/subagents/REFERENCE.md +189 -0
- package/skills/subagents/SKILL.md +2 -2
- package/skills/workflows/REFERENCE.md +10 -5
- package/skills/workflows/SKILL.md +53 -10
- package/web/adapter/pi-adapter.ts +661 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +789 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +462 -0
- package/web/runtime/pi-runtime.ts +991 -0
- package/web/runtime/types.ts +71 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1398 -0
- package/web/ui/index.html +139 -0
- package/web/ui/styles.css +598 -0
- package/web/vite.config.mjs +34 -0
- package/extensions/execution-convergence/active-evidence.ts +0 -129
- package/extensions/execution-convergence/index.ts +0 -442
- package/extensions/execution-convergence/workspace-provenance.ts +0 -338
- package/extensions/setup/intercom-fs-helper.cjs +0 -130
- package/extensions/setup/intercom.ts +0 -603
- package/extensions/subagents/src/backends/stub.ts +0 -296
- package/extensions/subagents/src/format.ts +0 -48
|
@@ -3,8 +3,12 @@ import {
|
|
|
3
3
|
keyHint,
|
|
4
4
|
type Theme,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import {
|
|
7
|
+
type Component,
|
|
8
|
+
Markdown,
|
|
9
|
+
truncateToWidth,
|
|
10
|
+
} from "@earendil-works/pi-tui";
|
|
11
|
+
import { sanitizeText } from "../../../shared/agent-transcript.ts";
|
|
8
12
|
|
|
9
13
|
const MAX_STATUS_ROWS = 4;
|
|
10
14
|
|
|
@@ -12,40 +16,116 @@ export interface WaitResultItem {
|
|
|
12
16
|
readonly id: string;
|
|
13
17
|
readonly title?: string;
|
|
14
18
|
readonly status?: string;
|
|
19
|
+
readonly outcome?: "completed" | "failed" | "interrupted";
|
|
20
|
+
readonly worktreeBranch?: string;
|
|
21
|
+
readonly elapsed?: string;
|
|
22
|
+
readonly artifactSaveFailed?: boolean;
|
|
23
|
+
readonly fullResultSaved?: boolean;
|
|
15
24
|
}
|
|
16
25
|
|
|
17
26
|
export interface WaitResultDetails {
|
|
18
27
|
readonly results?: readonly WaitResultItem[];
|
|
19
28
|
}
|
|
20
29
|
|
|
30
|
+
function singleLine(value: string) {
|
|
31
|
+
return sanitizeText(value).replace(/\s+/gu, " ").trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fixedRows(rows: readonly string[]): Component {
|
|
35
|
+
return {
|
|
36
|
+
render(width) {
|
|
37
|
+
return rows.map((row) => truncateToWidth(row, Math.max(1, width), "…"));
|
|
38
|
+
},
|
|
39
|
+
invalidate() {},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function selectStatusRows(results: readonly WaitResultItem[]) {
|
|
44
|
+
const visible = results.slice(0, MAX_STATUS_ROWS);
|
|
45
|
+
if (visible.some((result) => result.status === "error")) {
|
|
46
|
+
return visible;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const hiddenFailure = results
|
|
50
|
+
.slice(MAX_STATUS_ROWS)
|
|
51
|
+
.find((result) => result.status === "error");
|
|
52
|
+
return hiddenFailure ? [...visible.slice(0, -1), hiddenFailure] : visible;
|
|
53
|
+
}
|
|
54
|
+
|
|
21
55
|
export function buildWaitResultPreview(
|
|
22
56
|
content: string,
|
|
23
57
|
details: WaitResultDetails | undefined,
|
|
24
58
|
theme: Theme,
|
|
25
59
|
) {
|
|
26
60
|
const results = details?.results ?? [];
|
|
27
|
-
const
|
|
61
|
+
const uncertain = results.filter(
|
|
62
|
+
(result) => result.outcome === "interrupted",
|
|
63
|
+
).length;
|
|
64
|
+
const failed = results.filter(
|
|
65
|
+
(result) => result.status === "error" && result.outcome !== "interrupted",
|
|
66
|
+
).length;
|
|
67
|
+
const artifactFailures = results.filter(
|
|
68
|
+
(result) => result.artifactSaveFailed,
|
|
69
|
+
).length;
|
|
70
|
+
const exceptions = [
|
|
71
|
+
failed > 0 ? theme.fg("error", `${failed} failed`) : "",
|
|
72
|
+
uncertain > 0 ? theme.fg("warning", `${uncertain} uncertain`) : "",
|
|
73
|
+
].filter(Boolean);
|
|
28
74
|
const header =
|
|
29
|
-
theme.fg(
|
|
30
|
-
|
|
31
|
-
|
|
75
|
+
theme.fg(
|
|
76
|
+
failed > 0 || uncertain > 0 ? "warning" : "success",
|
|
77
|
+
failed > 0 || uncertain > 0 ? "!" : "✓",
|
|
78
|
+
) +
|
|
79
|
+
` ${exceptions.length > 0 ? `${exceptions.join(" · ")} · ` : ""}` +
|
|
80
|
+
theme.fg(
|
|
81
|
+
"accent",
|
|
82
|
+
theme.bold(
|
|
83
|
+
`${results.length} subagent${results.length === 1 ? "" : "s"} settled`,
|
|
84
|
+
),
|
|
85
|
+
) +
|
|
86
|
+
(artifactFailures > 0
|
|
87
|
+
? theme.fg(
|
|
88
|
+
"warning",
|
|
89
|
+
` · ${artifactFailures} artifact${artifactFailures === 1 ? "" : "s"} not saved`,
|
|
90
|
+
)
|
|
91
|
+
: "");
|
|
32
92
|
const lines = [header];
|
|
93
|
+
const statusRows = selectStatusRows(results);
|
|
33
94
|
|
|
34
|
-
for (const result of
|
|
35
|
-
const
|
|
95
|
+
for (const result of statusRows) {
|
|
96
|
+
const isUncertain = result.outcome === "interrupted";
|
|
97
|
+
const isFailure = result.status === "error" && !isUncertain;
|
|
36
98
|
const icon = theme.fg(
|
|
37
|
-
isFailure ? "error" : "success",
|
|
38
|
-
isFailure ? "x" : "✓",
|
|
99
|
+
isFailure ? "error" : isUncertain ? "warning" : "success",
|
|
100
|
+
isFailure ? "x" : isUncertain ? "?" : "✓",
|
|
39
101
|
);
|
|
102
|
+
const id = singleLine(result.id);
|
|
103
|
+
const title = result.title ? singleLine(result.title) : "";
|
|
104
|
+
const status = isUncertain
|
|
105
|
+
? "uncertain"
|
|
106
|
+
: singleLine(result.status ?? "settled");
|
|
107
|
+
const elapsed = result.elapsed ? singleLine(result.elapsed) : "";
|
|
108
|
+
const notices = [
|
|
109
|
+
result.artifactSaveFailed ? "artifact not saved" : "",
|
|
110
|
+
result.worktreeBranch
|
|
111
|
+
? `worktree handoff · ${singleLine(result.worktreeBranch)}`
|
|
112
|
+
: "",
|
|
113
|
+
result.fullResultSaved ? "full result saved" : "",
|
|
114
|
+
].filter(Boolean);
|
|
115
|
+
const attention = notices.length
|
|
116
|
+
? theme.fg("warning", `${notices.join(" · ")} · `)
|
|
117
|
+
: "";
|
|
40
118
|
lines.push(
|
|
41
|
-
` ${icon} ${theme.fg("accent",
|
|
119
|
+
` ${icon} ${attention}${theme.fg("accent", id)}${title ? theme.fg("muted", ` · ${title}`) : ""}${theme.fg("dim", ` · ${status}${elapsed ? ` · ${elapsed}` : ""}`)}`,
|
|
42
120
|
);
|
|
43
121
|
}
|
|
44
|
-
if (results.length >
|
|
45
|
-
lines.push(
|
|
122
|
+
if (results.length > statusRows.length) {
|
|
123
|
+
lines.push(
|
|
124
|
+
theme.fg("dim", ` … ${results.length - statusRows.length} more`),
|
|
125
|
+
);
|
|
46
126
|
}
|
|
47
127
|
|
|
48
|
-
if (content.trim()) {
|
|
128
|
+
if (sanitizeText(content).trim()) {
|
|
49
129
|
lines.push(
|
|
50
130
|
theme.fg(
|
|
51
131
|
"dim",
|
|
@@ -56,6 +136,14 @@ export function buildWaitResultPreview(
|
|
|
56
136
|
return lines.join("\n");
|
|
57
137
|
}
|
|
58
138
|
|
|
139
|
+
export function renderWaitResultPreview(
|
|
140
|
+
content: string,
|
|
141
|
+
details: WaitResultDetails | undefined,
|
|
142
|
+
theme: Theme,
|
|
143
|
+
) {
|
|
144
|
+
return fixedRows(buildWaitResultPreview(content, details, theme).split("\n"));
|
|
145
|
+
}
|
|
146
|
+
|
|
59
147
|
export function renderWaitResult(
|
|
60
148
|
content: string,
|
|
61
149
|
details: WaitResultDetails | undefined,
|
|
@@ -63,7 +151,7 @@ export function renderWaitResult(
|
|
|
63
151
|
theme: Theme,
|
|
64
152
|
) {
|
|
65
153
|
if (!expanded) {
|
|
66
|
-
return
|
|
154
|
+
return renderWaitResultPreview(content, details, theme);
|
|
67
155
|
}
|
|
68
156
|
|
|
69
157
|
const markdown = new Markdown(
|
|
@@ -13,6 +13,10 @@ import {
|
|
|
13
13
|
} from "../../shared/below-editor-navigation.ts";
|
|
14
14
|
|
|
15
15
|
const FAKE_CURSOR_PATTERN = /\u001b\[7m \u001b\[(?:0|27)m/;
|
|
16
|
+
// Conservative idle-row band for terminal-owned CJK IME preedit. Pi's editor
|
|
17
|
+
// contract exposes only committed input, never a composition event, so the
|
|
18
|
+
// reservation cannot shrink to the frames that actually need it. A 1-cell
|
|
19
|
+
// hardware-cursor pad lets long preedit cover or truncate the ghost.
|
|
16
20
|
const IME_PREEDIT_MIN_COLUMNS = 12;
|
|
17
21
|
const IME_PREEDIT_MAX_COLUMNS = 32;
|
|
18
22
|
const GHOST_MIN_COLUMNS = 8;
|
|
@@ -96,10 +100,12 @@ function ghostGeometry(lines: readonly string[], width: number) {
|
|
|
96
100
|
.replaceAll(CURSOR_MARKER, "");
|
|
97
101
|
const prefix = `${beforeCursor}${cursor[0]}`;
|
|
98
102
|
const remaining = Math.max(0, width - visibleWidth(prefix));
|
|
99
|
-
//
|
|
100
|
-
// narrower widths, suppress the ghost rather than placing the
|
|
101
|
-
// terminal's out-of-range column width.
|
|
102
|
-
if (remaining
|
|
103
|
+
// Keep enough cells for a readable ghost and a conservative IME preedit
|
|
104
|
+
// band. At narrower widths, suppress the ghost rather than placing the
|
|
105
|
+
// hidden hardware cursor at the terminal's out-of-range column width.
|
|
106
|
+
if (remaining < GHOST_MIN_COLUMNS + IME_PREEDIT_MIN_COLUMNS) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
103
109
|
|
|
104
110
|
const desiredPreedit = Math.min(
|
|
105
111
|
IME_PREEDIT_MAX_COLUMNS,
|
|
@@ -34,7 +34,6 @@ import {
|
|
|
34
34
|
type TaskToolDetails,
|
|
35
35
|
} from "./ui.ts";
|
|
36
36
|
|
|
37
|
-
const TOOL_NAMES = ["tasks_add", "tasks_update", "tasks_list"] as const;
|
|
38
37
|
const TASK_WIDGET_KEY = "session-tasks-panel";
|
|
39
38
|
const CONFLICT_NAMES = new Set(["todo", "TodoWrite", "update_plan"]);
|
|
40
39
|
const TOOL_PURPOSE =
|
|
@@ -542,8 +541,6 @@ export default function sessionTasks(pi: ExtensionAPI) {
|
|
|
542
541
|
});
|
|
543
542
|
}
|
|
544
543
|
|
|
545
|
-
export { TOOL_NAMES as TASK_TOOL_NAMES };
|
|
546
|
-
|
|
547
544
|
function mutationWillCloseBatch(
|
|
548
545
|
snapshot: TaskSnapshot,
|
|
549
546
|
id: number,
|
package/extensions/tasks/ui.ts
CHANGED
|
@@ -4,15 +4,35 @@ import type {
|
|
|
4
4
|
Theme,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import {
|
|
7
|
+
type Component,
|
|
7
8
|
Key,
|
|
8
9
|
matchesKey,
|
|
9
10
|
Text,
|
|
10
|
-
truncateToWidth,
|
|
11
|
-
type Component,
|
|
12
11
|
type TUI,
|
|
12
|
+
truncateToWidth,
|
|
13
|
+
visibleWidth,
|
|
13
14
|
} from "@earendil-works/pi-tui";
|
|
15
|
+
import { fitNavigationSides } from "../shared/below-editor-navigation.ts";
|
|
16
|
+
import {
|
|
17
|
+
hintLine,
|
|
18
|
+
panelFrame,
|
|
19
|
+
screenTitleLine,
|
|
20
|
+
} from "../shared/screen-chrome.ts";
|
|
14
21
|
import type { TaskItem, TaskSnapshot } from "./tasks.ts";
|
|
15
22
|
|
|
23
|
+
/**
|
|
24
|
+
* One colour per status, shared by every surface. The widget used to paint
|
|
25
|
+
* in-progress amber while the full list painted it accent, so the same item
|
|
26
|
+
* changed colour depending on where you looked at it.
|
|
27
|
+
*/
|
|
28
|
+
const STATUS_COLOR = {
|
|
29
|
+
pending: "muted",
|
|
30
|
+
in_progress: "accent",
|
|
31
|
+
blocked: "warning",
|
|
32
|
+
done: "success",
|
|
33
|
+
dropped: "error",
|
|
34
|
+
} as const satisfies Record<TaskItem["status"], string>;
|
|
35
|
+
|
|
16
36
|
const STATUS_ICON: Record<TaskItem["status"], string> = {
|
|
17
37
|
pending: "○",
|
|
18
38
|
in_progress: "●",
|
|
@@ -86,45 +106,41 @@ export function taskCounts(items: readonly TaskItem[]): TaskCounts {
|
|
|
86
106
|
}
|
|
87
107
|
|
|
88
108
|
/**
|
|
89
|
-
* One-line census: `4 tasks
|
|
109
|
+
* One-line census: `4 tasks · 3 done · 1 open`.
|
|
90
110
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
111
|
+
* Colour carries the status and the total anchors the line, so nothing needs
|
|
112
|
+
* bold numbers alternating with dim words — that zebra was the loudest thing
|
|
113
|
+
* on screen and said the least. Zeros are dropped: "0 in progress" costs a
|
|
114
|
+
* segment to tell you nothing, and a segment appearing when work starts is a
|
|
115
|
+
* signal, not a glitch. When one status covers everything the redundant count
|
|
116
|
+
* collapses to `all`, so a fresh batch reads `8 tasks · all open` rather than
|
|
117
|
+
* `8 tasks · 8 open`.
|
|
96
118
|
*
|
|
97
119
|
* Takes counts rather than items because a view often shows a bounded subset
|
|
98
120
|
* of rows; the header must describe the whole batch regardless.
|
|
99
121
|
*/
|
|
100
122
|
export function renderTaskSummary(counts: TaskCounts, theme: Theme): string {
|
|
101
|
-
const number = (value: number) => theme.bold(theme.fg("text", String(value)));
|
|
102
|
-
const dim = (text: string) => theme.fg("dim", text);
|
|
103
123
|
// Coerced, not trusted: these counts can arrive from a tool-result record
|
|
104
124
|
// persisted by an older build, where a missing key would render the literal
|
|
105
125
|
// word "undefined" (or "NaN tasks") into the header.
|
|
106
126
|
const count = (status: TaskItem["status"]) =>
|
|
107
127
|
Number.isFinite(counts[status]) ? counts[status] : 0;
|
|
108
128
|
const total = Number.isFinite(counts.total) ? counts.total : 0;
|
|
129
|
+
if (total <= 0) return theme.fg("dim", "no tasks");
|
|
130
|
+
const present = SUMMARY_ORDER.filter((status) => count(status) > 0);
|
|
109
131
|
// Built segment by segment rather than by wrapping the whole line: each
|
|
110
132
|
// styled run emits its own reset, so an outer color would stop applying at
|
|
111
133
|
// the first inner one.
|
|
112
|
-
const
|
|
113
|
-
(
|
|
114
|
-
status
|
|
115
|
-
status === "
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
).map((status) => `${number(count(status))} ${dim(SUMMARY_LABEL[status])}`);
|
|
134
|
+
const chips = present.map((status) =>
|
|
135
|
+
theme.fg(
|
|
136
|
+
STATUS_COLOR[status],
|
|
137
|
+
`${present.length === 1 && count(status) === total ? "all" : count(status)} ${SUMMARY_LABEL[status]}`,
|
|
138
|
+
),
|
|
139
|
+
);
|
|
119
140
|
return [
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
" ",
|
|
124
|
-
dim("("),
|
|
125
|
-
parts.join(dim(", ")),
|
|
126
|
-
dim(")"),
|
|
127
|
-
].join("");
|
|
141
|
+
theme.fg("dim", `${total} ${total === 1 ? "task" : "tasks"}`),
|
|
142
|
+
...chips,
|
|
143
|
+
].join(theme.fg("dim", " · "));
|
|
128
144
|
}
|
|
129
145
|
|
|
130
146
|
export interface TaskToolDetails {
|
|
@@ -150,16 +166,7 @@ export function renderTaskRows(
|
|
|
150
166
|
// (all of them) — would otherwise shift every subject sideways by a column.
|
|
151
167
|
const idWidth = Math.max(...items.map((item) => `T${item.id}`.length), 3);
|
|
152
168
|
return items.flatMap((item) => {
|
|
153
|
-
const color =
|
|
154
|
-
item.status === "done"
|
|
155
|
-
? "success"
|
|
156
|
-
: item.status === "blocked"
|
|
157
|
-
? "warning"
|
|
158
|
-
: item.status === "dropped"
|
|
159
|
-
? "error"
|
|
160
|
-
: item.status === "in_progress"
|
|
161
|
-
? "accent"
|
|
162
|
-
: "muted";
|
|
169
|
+
const color = STATUS_COLOR[item.status];
|
|
163
170
|
// No `[status]` text: the icon, its color, and the subject's own weight
|
|
164
171
|
// already say it, and repeating it in words crowded every row.
|
|
165
172
|
const id = `T${item.id}`.padStart(idWidth);
|
|
@@ -215,17 +222,23 @@ export function renderTaskWidget(
|
|
|
215
222
|
|
|
216
223
|
const hasOverflow = actionable.length > TASK_WIDGET_LIMIT;
|
|
217
224
|
const toggleHint = hasOverflow
|
|
218
|
-
? `
|
|
225
|
+
? ` · ctrl+shift+t ${expanded ? "collapse" : "show all"}`
|
|
219
226
|
: "";
|
|
220
227
|
// Same census as the full list and the /tasks screen. Counted over `tracked`
|
|
221
228
|
// rather than every item, because the widget deliberately hides dropped work
|
|
222
229
|
// and a total that included it would not add up against the rows shown.
|
|
230
|
+
//
|
|
231
|
+
// Hints sit on the right edge instead of trailing the census, so the eye lands
|
|
232
|
+
// on state first and the keystrokes stay out of the way until wanted. They are
|
|
233
|
+
// dropped rather than truncated when the terminal is too narrow to hold both.
|
|
234
|
+
const label =
|
|
235
|
+
theme.fg("accent", "◆ ") + theme.fg("text", theme.bold("Tasks"));
|
|
236
|
+
const left = `${label} ${renderTaskSummary(taskCounts(tracked), theme)}`;
|
|
237
|
+
const hint = theme.fg("dim", `/tasks${toggleHint}`);
|
|
223
238
|
const header =
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
renderTaskSummary(taskCounts(tracked), theme) +
|
|
228
|
-
theme.fg("dim", ` · /tasks${toggleHint}`);
|
|
239
|
+
visibleWidth(left) + visibleWidth(hint) + 3 <= width
|
|
240
|
+
? fitNavigationSides(left, hint, width)
|
|
241
|
+
: left;
|
|
229
242
|
const visible = expanded
|
|
230
243
|
? actionable
|
|
231
244
|
: actionable.slice(0, TASK_WIDGET_LIMIT);
|
|
@@ -235,12 +248,7 @@ export function renderTaskWidget(
|
|
|
235
248
|
const idWidth = Math.max(...visible.map((i) => `T${i.id}`.length), 3);
|
|
236
249
|
const lines = [truncateToWidth(header, width)];
|
|
237
250
|
for (const [index, item] of visible.entries()) {
|
|
238
|
-
const color =
|
|
239
|
-
item.status === "in_progress"
|
|
240
|
-
? "warning"
|
|
241
|
-
: item.status === "blocked"
|
|
242
|
-
? "error"
|
|
243
|
-
: "muted";
|
|
251
|
+
const color = STATUS_COLOR[item.status];
|
|
244
252
|
const branch = index === visible.length - 1 && hidden === 0 ? "╰─" : "├─";
|
|
245
253
|
lines.push(
|
|
246
254
|
truncateToWidth(
|
|
@@ -379,27 +387,36 @@ class TasksScreen implements Component {
|
|
|
379
387
|
}
|
|
380
388
|
|
|
381
389
|
render(width: number) {
|
|
382
|
-
const
|
|
383
|
-
const
|
|
390
|
+
const theme = this.theme;
|
|
391
|
+
const counts = taskCounts(this.snapshot.items);
|
|
392
|
+
const body = renderTaskRows(this.snapshot.items, theme, width - 4);
|
|
393
|
+
// Title (1) + frame (2) + hint (1): one row more chrome than the old bare
|
|
394
|
+
// rule, so the body gives one back and the screen keeps its total height.
|
|
395
|
+
const rows = Math.max(8, (this.tui.terminal.rows || 30) - 9);
|
|
384
396
|
const maxOffset = Math.max(0, body.length - rows);
|
|
385
397
|
this.offset = Math.min(this.offset, maxOffset);
|
|
386
398
|
const visible = body.slice(this.offset, this.offset + rows);
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
399
|
+
// Framed like /subagents, /ps, and /workflows rather than a bare rule: a
|
|
400
|
+
// full-screen view of a list is the same object in each of them, and it
|
|
401
|
+
// should not look like a different control here.
|
|
402
|
+
return [
|
|
403
|
+
screenTitleLine(theme, "Session tasks", "", width),
|
|
404
|
+
...panelFrame(theme, {
|
|
405
|
+
label: renderTaskSummary(counts, theme),
|
|
406
|
+
rows: visible.map((line) => ` ${line}`),
|
|
390
407
|
width,
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
408
|
+
height: rows + 2,
|
|
409
|
+
}),
|
|
410
|
+
hintLine(
|
|
411
|
+
theme,
|
|
412
|
+
[
|
|
413
|
+
["j/k or ↑/↓", "scroll"],
|
|
414
|
+
["pgup/pgdn", "page"],
|
|
415
|
+
["esc", "close"],
|
|
416
|
+
],
|
|
399
417
|
width,
|
|
400
418
|
),
|
|
401
|
-
|
|
402
|
-
return lines;
|
|
419
|
+
];
|
|
403
420
|
}
|
|
404
421
|
|
|
405
422
|
invalidate() {}
|
|
@@ -28,6 +28,9 @@ const ESCAPE_PATTERN = /\u001b(?:[()][0-2A-Z]|[ -/]*[@-~])/g;
|
|
|
28
28
|
|
|
29
29
|
const RESET = "\x1b[0m";
|
|
30
30
|
const POWERLINE_ARROW = "\ue0b0"; // — decorative; text remains readable without Nerd Font
|
|
31
|
+
const MODEL_ICON = "\uec10"; // Codicon: sparkle
|
|
32
|
+
const CONTEXT_ICON = "\uebe4"; // Codicon: pie-chart
|
|
33
|
+
const DIRECTORY_ICON = "\uea83"; // Codicon: folder
|
|
31
34
|
|
|
32
35
|
/** Higher = keep longer when the line is too narrow. */
|
|
33
36
|
const PRIORITY: Record<FooterItem, number> = {
|
|
@@ -177,11 +180,11 @@ export function buildSegmentCatalog(
|
|
|
177
180
|
: modelInfo.modelId;
|
|
178
181
|
|
|
179
182
|
return {
|
|
180
|
-
cwd: { text: formatDirectory(cwd)
|
|
181
|
-
model: { text: modelText
|
|
183
|
+
cwd: { text: `${DIRECTORY_ICON} ${formatDirectory(cwd)}`, tone: "text" },
|
|
184
|
+
model: { text: `${MODEL_ICON} ${modelText}`, tone: "muted" },
|
|
182
185
|
thinking: { text: modelInfo.thinking, tone: "muted" },
|
|
183
186
|
context: {
|
|
184
|
-
text: contextText,
|
|
187
|
+
text: contextText ? `${CONTEXT_ICON} ${contextText}` : "",
|
|
185
188
|
tone: contextTone(modelInfo.contextPercent),
|
|
186
189
|
},
|
|
187
190
|
cache: {
|
|
@@ -199,7 +202,7 @@ export function buildSegmentCatalog(
|
|
|
199
202
|
: `~${Math.round(modelInfo.tokensPerSecond)} tok/s`,
|
|
200
203
|
tone: "muted",
|
|
201
204
|
},
|
|
202
|
-
git: { text: gitInfo.branch
|
|
205
|
+
git: { text: gitInfo.branch ? `⎇ ${gitInfo.branch}` : "", tone: "muted" },
|
|
203
206
|
pr: {
|
|
204
207
|
text: gitInfo.pullRequest
|
|
205
208
|
? formatPullRequest(gitInfo.pullRequest.number, gitInfo.pullRequest.url)
|
|
@@ -524,43 +527,3 @@ export function renderFooter(options: FooterRenderOptions) {
|
|
|
524
527
|
|
|
525
528
|
return lines;
|
|
526
529
|
}
|
|
527
|
-
|
|
528
|
-
/** Legacy helper retained for tests that assert grouped content. */
|
|
529
|
-
export function buildFooterContent(
|
|
530
|
-
modelInfo: ModelInfoState,
|
|
531
|
-
gitInfo: GitInfoState,
|
|
532
|
-
selectedItems: readonly FooterItem[],
|
|
533
|
-
formatPullRequest: (number: number, url: string) => string = (number) =>
|
|
534
|
-
`PR #${number}`,
|
|
535
|
-
) {
|
|
536
|
-
const items = new Set(selectedItems);
|
|
537
|
-
const catalog = buildSegmentCatalog(
|
|
538
|
-
"",
|
|
539
|
-
modelInfo,
|
|
540
|
-
gitInfo,
|
|
541
|
-
formatPullRequest,
|
|
542
|
-
);
|
|
543
|
-
return {
|
|
544
|
-
showCwd: items.has("cwd"),
|
|
545
|
-
model: [
|
|
546
|
-
items.has("model") ? catalog.model.text : "",
|
|
547
|
-
items.has("thinking") ? catalog.thinking.text : "",
|
|
548
|
-
]
|
|
549
|
-
.filter(Boolean)
|
|
550
|
-
.join(" · "),
|
|
551
|
-
usage: [
|
|
552
|
-
items.has("context") ? catalog.context.text : "",
|
|
553
|
-
items.has("cache") ? catalog.cache.text : "",
|
|
554
|
-
items.has("cost") ? catalog.cost.text : "",
|
|
555
|
-
items.has("throughput") ? catalog.throughput.text : "",
|
|
556
|
-
]
|
|
557
|
-
.filter(Boolean)
|
|
558
|
-
.join(" · "),
|
|
559
|
-
git: [
|
|
560
|
-
items.has("git") ? catalog.git.text : "",
|
|
561
|
-
items.has("pr") ? catalog.pr.text : "",
|
|
562
|
-
]
|
|
563
|
-
.filter(Boolean)
|
|
564
|
-
.join(" · "),
|
|
565
|
-
};
|
|
566
|
-
}
|
|
@@ -21,13 +21,9 @@ import {
|
|
|
21
21
|
REFRESH_CHANNEL,
|
|
22
22
|
isGitInfoState,
|
|
23
23
|
isModelInfoState,
|
|
24
|
-
type GitInfoState,
|
|
25
|
-
type ModelInfoState,
|
|
26
24
|
} from "../shared/dashboard-state.ts";
|
|
27
25
|
import { formatDirectory, renderFooter } from "./footer.ts";
|
|
28
26
|
|
|
29
|
-
export { buildFooterContent, formatDirectory, renderFooter } from "./footer.ts";
|
|
30
|
-
|
|
31
27
|
type Rgb = [number, number, number];
|
|
32
28
|
|
|
33
29
|
const RESET = "\x1b[0m";
|