pi-crew 0.11.6 → 0.11.8
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/.githooks/pre-commit +22 -0
- package/.githooks/pre-push +56 -0
- package/CHANGELOG.md +216 -1
- package/dist/index.mjs +531 -508
- package/docs/publishing.md +29 -0
- package/index.ts +3 -3
- package/package.json +9 -6
- package/scripts/install-hooks.mjs +32 -0
- package/skills/real-test-pi-crew/SKILL.md +31 -18
- package/src/config/config-validation.ts +35 -0
- package/src/config/env-vars.ts +38 -0
- package/src/config/types.ts +71 -0
- package/src/extension/registration/foreground-run-controller.ts +19 -10
- package/src/extension/registration/lifecycle-handlers.ts +6 -491
- package/src/extension/registration/lifecycle.ts +7 -90
- package/src/extension/registration/render-loop.ts +502 -0
- package/src/extension/team-tool/doctor.ts +6 -0
- package/src/prompt/prompt-runtime.ts +33 -4
- package/src/runtime/child-pi/child-pi-spawn.ts +14 -0
- package/src/runtime/child-pi/child-pi.ts +130 -6
- package/src/runtime/child-pi/pi-version.ts +187 -0
- package/src/runtime/child-pi/session-recovery.ts +334 -0
- package/src/runtime/classifier/classifier-service.ts +298 -0
- package/src/runtime/dispatch-batch.ts +4 -0
- package/src/runtime/live-session/live-session-runtime.ts +31 -0
- package/src/runtime/model/model-scope.ts +141 -21
- package/src/runtime/model/pi-args.ts +166 -6
- package/src/runtime/peer-dep.ts +24 -0
- package/src/runtime/process/zombie-scanner.ts +168 -7
- package/src/runtime/rpc/README.md +139 -0
- package/src/runtime/rpc/frame-client.ts +328 -0
- package/src/runtime/rpc/rpc-worker.ts +271 -0
- package/src/runtime/rpc/ui-request-policy.ts +112 -0
- package/src/runtime/run-worker.ts +91 -3
- package/src/runtime/scheduling/run-coalesced-task-group.ts +3 -0
- package/src/runtime/skill-instructions.ts +66 -1
- package/src/runtime/surface/resolve-surface.ts +34 -19
- package/src/runtime/surface/surface-spawn.ts +8 -1
- package/src/runtime/task-output-context.ts +191 -32
- package/src/runtime/task-packet.ts +3 -0
- package/src/runtime/task-runner/child-executor.ts +172 -2
- package/src/runtime/task-runner/post-execution.ts +5 -3
- package/src/runtime/task-runner/pre-execution.ts +32 -1
- package/src/runtime/task-runner/prompt-builder.ts +10 -2
- package/src/runtime/task-runner.ts +6 -0
- package/src/schema/config-schema.ts +37 -0
- package/src/schema/team-tool-schema.ts +14 -2
- package/src/state/contracts.ts +5 -0
- package/src/ui/format-helpers.ts +11 -0
- package/src/ui/overlays/mailbox-compose-overlay.ts +15 -1
- package/src/ui/pi-ui-compat.ts +18 -9
- package/src/ui/rail.ts +14 -1
- package/src/ui/settings-overlay.ts +11 -2
- package/src/ui/widget/index.ts +0 -1
- package/src/ui/widget/task-list.ts +10 -3
- package/src/ui/widget/widget-renderer.ts +53 -4
- package/src/utils/relative-time.ts +11 -3
- package/src/utils/visual.ts +32 -2
|
@@ -75,6 +75,29 @@ export const PiTeamsRuntimeConfigSchema = Type.Object(
|
|
|
75
75
|
graceTurns: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
76
76
|
taskTimeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
77
77
|
inheritContext: Type.Optional(Type.Boolean({ sensitive: true })),
|
|
78
|
+
// W2 (session-file recovery): when a worker dies without a final assistant
|
|
79
|
+
// event (exitCode null / killed), tail-replay its session JSONL and attach
|
|
80
|
+
// the last complete assistant record to the run result. Default ON; env
|
|
81
|
+
// PI_CREW_SESSION_RECOVERY overrides. Kill switch, not a secret — no
|
|
82
|
+
// sensitive mark (same policy as surface: picks behavior, not authority).
|
|
83
|
+
sessionRecovery: Type.Optional(Type.Boolean()),
|
|
84
|
+
// R3-19/D5 reversal: hermetic worker spawns (--no-extensions on worker
|
|
85
|
+
// argv; explicit -e prompt-runtime + --skill flags unaffected). Default
|
|
86
|
+
// TRUE — off-switch for operators who want the old ambient-stack parity.
|
|
87
|
+
// Env PI_CREW_HERMETIC_WORKERS overrides either way. Behavior pick, not
|
|
88
|
+
// authority — no sensitive mark.
|
|
89
|
+
hermeticWorkers: Type.Optional(Type.Boolean()),
|
|
90
|
+
// P2-1 (pi 1.0.4 adoption): host-process retry-triage classifier seam
|
|
91
|
+
// (src/runtime/classifier/). DORMANT — default FALSE; enabling turns on
|
|
92
|
+
// transient-vs-permanent classification before expensive re-attempts.
|
|
93
|
+
// Env PI_CREW_CLASSIFIER_ENABLED overrides either way. Behavior pick,
|
|
94
|
+
// not authority — no sensitive mark.
|
|
95
|
+
classifierEnabled: Type.Optional(Type.Boolean()),
|
|
96
|
+
// P2-1: classifier model id for the seam above. Default resolved at the
|
|
97
|
+
// consumer (resolveClassifierModel → "opencode/jev-1.13-free"); env
|
|
98
|
+
// PI_CREW_CLASSIFIER_MODEL overrides. Catalog ids only — availability
|
|
99
|
+
// depends on provider credentials at runtime.
|
|
100
|
+
classifierModel: Type.Optional(Type.String({ minLength: 1 })),
|
|
78
101
|
promptMode: Type.Optional(Type.Union([Type.Literal("replace"), Type.Literal("append")])),
|
|
79
102
|
groupJoin: Type.Optional(Type.Union([Type.Literal("off"), Type.Literal("group"), Type.Literal("smart")])),
|
|
80
103
|
groupJoinAckTimeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
@@ -94,6 +117,10 @@ export const PiTeamsRuntimeConfigSchema = Type.Object(
|
|
|
94
117
|
),
|
|
95
118
|
),
|
|
96
119
|
excludeContextBash: Type.Optional(Type.Boolean()),
|
|
120
|
+
// Handoff budget: est-token (chars/4) cap on the dynamic.dependencyContext
|
|
121
|
+
// prompt layer. Matches parseHandoffBudgetTokens in config-validation.ts
|
|
122
|
+
// (out-of-range values are dropped → render default 1800).
|
|
123
|
+
handoffBudgetTokens: Type.Optional(Type.Integer({ minimum: 1, maximum: 1_000_000 })),
|
|
97
124
|
agentExtensions: Type.Optional(Type.Array(Type.String({ minLength: 1 }), { sensitive: true })),
|
|
98
125
|
// Mux-surface policy (spec v0.7 §8.2.4): no sensitive marks — surface
|
|
99
126
|
// picks where worker processes live, not what they may do.
|
|
@@ -118,6 +145,16 @@ export const PiTeamsRuntimeConfigSchema = Type.Object(
|
|
|
118
145
|
),
|
|
119
146
|
),
|
|
120
147
|
modelFallback: Type.Optional(PiTeamsModelFallbackConfigSchema),
|
|
148
|
+
// W7 (P2-3) EXPERIMENTAL: worker transport selector, WIRED at the
|
|
149
|
+
// run-worker seam since e2728f68 (D1/DR2 plumbing, 2026-10-06): "rpc"
|
|
150
|
+
// selects the prototype RPC frame-client transport (src/runtime/rpc/),
|
|
151
|
+
// "stdio" (absent/default) is the production child-pi path. Env
|
|
152
|
+
// PI_CREW_WORKER_TRANSPORT=rpc overrides this config (env wins —
|
|
153
|
+
// established precedence; an invalid env value fails safe to stdio).
|
|
154
|
+
// Callers carrying runtimeConfig (child-executor, run-coalesced-task-
|
|
155
|
+
// group) thread it to the seam; callers without runtimeConfig keep the
|
|
156
|
+
// default. Not sensitive — picks a transport, not authority.
|
|
157
|
+
workerTransport: Type.Optional(Type.Union([Type.Literal("stdio"), Type.Literal("rpc")])),
|
|
121
158
|
},
|
|
122
159
|
{ additionalProperties: false },
|
|
123
160
|
);
|
|
@@ -280,6 +280,14 @@ const sharedFields = {
|
|
|
280
280
|
description: "Mark certain bash commands as excludeFromContext to reduce context tokens (default: false).",
|
|
281
281
|
}),
|
|
282
282
|
),
|
|
283
|
+
handoffBudgetTokens: Type.Optional(
|
|
284
|
+
Type.Integer({
|
|
285
|
+
minimum: 1,
|
|
286
|
+
maximum: 1_000_000,
|
|
287
|
+
description:
|
|
288
|
+
"Est-token (chars/4) budget cap on the dependency-context layer of worker prompts (default 1800). When exceeded, later dependencies are trimmed to summary head + artifact pointer. Out-of-range values are ignored.",
|
|
289
|
+
}),
|
|
290
|
+
),
|
|
283
291
|
budgetTotal: Type.Optional(
|
|
284
292
|
// Empty-string unset marker accepted (Tier-9: models emit "" when unset).
|
|
285
293
|
// 0 accepted as "unset/disabled" (models emit 0 for off); still rejects 1-999
|
|
@@ -414,7 +422,7 @@ const sharedFields = {
|
|
|
414
422
|
focus: Type.Optional(
|
|
415
423
|
Type.String({
|
|
416
424
|
description:
|
|
417
|
-
"Sub-focus for the doctor action. 'zombies' runs a READ-ONLY scan for orphaned pi-crew sub-agent processes (identified by PI_CREW_KIND=subagent); it never kills and never matches the user's interactive main session.",
|
|
425
|
+
"Sub-focus for the doctor action. 'zombies' runs a READ-ONLY scan for orphaned pi-crew sub-agent processes (identified by PI_CREW_KIND=subagent); it never kills and never matches the user's interactive main session. Also WARN-lists foreign CLI-spawned pi processes (AI_AGENT=pi markers, R3-16) — report-only, never kill candidates.",
|
|
418
426
|
}),
|
|
419
427
|
),
|
|
420
428
|
};
|
|
@@ -568,7 +576,9 @@ export interface TeamToolParamsValue {
|
|
|
568
576
|
config?: Record<string, unknown>;
|
|
569
577
|
/** Sub-focus for the `doctor` action. `"zombies"` runs a READ-ONLY scan for
|
|
570
578
|
* orphaned pi-crew sub-agent processes (identified by PI_CREW_KIND=subagent);
|
|
571
|
-
* it never kills and never matches the user's interactive main session.
|
|
579
|
+
* it never kills and never matches the user's interactive main session. Also
|
|
580
|
+
* WARN-lists foreign CLI-spawned pi processes (AI_AGENT=pi markers, R3-16) —
|
|
581
|
+
* report-only, never kill candidates. */
|
|
572
582
|
focus?: string;
|
|
573
583
|
dryRun?: boolean;
|
|
574
584
|
confirm?: boolean;
|
|
@@ -596,6 +606,8 @@ export interface TeamToolParamsValue {
|
|
|
596
606
|
b?: number;
|
|
597
607
|
/** Mark certain bash commands as excludeFromContext to reduce context tokens (default: false). */
|
|
598
608
|
excludeContextBash?: boolean;
|
|
609
|
+
/** Est-token (chars/4) budget cap on the dependency-context layer of worker prompts (default 1800). Later dependencies get trimmed to summary head + artifact pointer when exceeded. */
|
|
610
|
+
handoffBudgetTokens?: number;
|
|
599
611
|
/** Total token budget for the run. When set, enables budget tracking (minimum 1000). */
|
|
600
612
|
budgetTotal?: number;
|
|
601
613
|
/** When true, skip budget enforcement entirely (explicit opt-out). */
|
package/src/state/contracts.ts
CHANGED
|
@@ -106,6 +106,11 @@ export const TEAM_EVENT_TYPES = [
|
|
|
106
106
|
"task.respond_delivered",
|
|
107
107
|
"task.respond_missed",
|
|
108
108
|
"task.retried",
|
|
109
|
+
// P2-1 (pi 1.0.4 adoption, 2026-10-07): retry-triage classifier decision —
|
|
110
|
+
// fired by child-executor's triageRetryWithClassifier ONLY when a real
|
|
111
|
+
// classifier answer informed the retry-vs-skip decision (dormant unless
|
|
112
|
+
// runtime.classifierEnabled). Diagnostic only — never transitions state.
|
|
113
|
+
"task.retry_triage",
|
|
109
114
|
// WP-2/R2 waiting-producer (ADR-0 2026-08-17-waiting-producer-ask item 10):
|
|
110
115
|
// `ask` tool lifecycle — requested on park acceptance, answered on delivery
|
|
111
116
|
// (mailbox or requeue+inject), timedout on deadline expiry (both the
|
package/src/ui/format-helpers.ts
CHANGED
|
@@ -12,6 +12,17 @@ export function formatTokens(n: number): string {
|
|
|
12
12
|
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Canonical generic duration formatter (QW#5 collision resolution):
|
|
17
|
+
* `45ms` / `12.3s` / `3m5s` / `75m` — minutes are unbounded (no hours
|
|
18
|
+
* bucket) and there is no invalid-input guard. Two same-stemmed helpers are
|
|
19
|
+
* DELIBERATELY separate dialects and must not be merged into this one
|
|
20
|
+
* (their output formats are design-pinned):
|
|
21
|
+
* - `formatTaskElapsed` (ui/widget/task-list.ts) — plan-card elapsed:
|
|
22
|
+
* `2m 49s` / `1h 03m`, empty string on invalid input.
|
|
23
|
+
* - `formatRelativeCompact` (utils/relative-time.ts) — relative-time
|
|
24
|
+
* ladder buckets `84m` / `14h` / `2d14h`.
|
|
25
|
+
*/
|
|
15
26
|
export function formatDuration(ms: number): string {
|
|
16
27
|
if (ms < 1000) return `${ms}ms`;
|
|
17
28
|
if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
|
|
@@ -14,11 +14,16 @@
|
|
|
14
14
|
* canopy/hint rows are NOT part of the split any more, so the hint keeps its
|
|
15
15
|
* full width instead of being cropped to 60% of the overlay.
|
|
16
16
|
*
|
|
17
|
+
* R3-6 (IME): the ACTIVE field emits pi-tui's zero-width `CURSOR_MARKER` at
|
|
18
|
+
* its value end — the host TUI strips it and parks the hardware cursor there
|
|
19
|
+
* so the IME candidate window follows the logical text cursor (CJK input).
|
|
20
|
+
*
|
|
17
21
|
* Keys/behaviour are unchanged (same `overlay:*` dispatch, same free-text
|
|
18
22
|
* passthrough, same validation errors, same discard confirmation). Hints come
|
|
19
23
|
* from `formatHint` (discard LAST, keys through `keyToken`).
|
|
20
24
|
*/
|
|
21
25
|
|
|
26
|
+
import { CURSOR_MARKER } from "@earendil-works/pi-tui";
|
|
22
27
|
import type { MailboxDirection } from "../../state/coordination/mailbox.ts";
|
|
23
28
|
import { pad, sanitizeLine, truncate } from "../../utils/visual.ts";
|
|
24
29
|
import { overlayActionForKey } from "../keybinding-map.ts";
|
|
@@ -117,7 +122,16 @@ export class MailboxComposeOverlay {
|
|
|
117
122
|
// (the field is a single-line cell; multi-line content belongs to the
|
|
118
123
|
// preview column).
|
|
119
124
|
const value = sanitizeLine(this.fields[field] ?? "");
|
|
120
|
-
|
|
125
|
+
// R3-6 (IME): CURSOR_MARKER is pi-tui's zero-width APC sequence — the host
|
|
126
|
+
// TUI finds it, positions the HARDWARE cursor there (IME candidate window
|
|
127
|
+
// anchor for CJK input), and strips it before painting. This overlay's
|
|
128
|
+
// fields append/backspace at end-of-value only, so the logical cursor IS
|
|
129
|
+
// the value end — same pattern as pi-tui's own Input component
|
|
130
|
+
// (marker + fake cursor). The `›` prefix stays: it is the RAIL list-selection
|
|
131
|
+
// glyph, orthogonal to the text cursor. Direction checkbox row gets NO
|
|
132
|
+
// marker — it toggles on space and has no text insertion point.
|
|
133
|
+
const marker = active && !this.confirm ? CURSOR_MARKER : "";
|
|
134
|
+
return `${active ? CURSOR : " "} ${label}: ${truncate(value, Math.max(8, width - label.length - 5))}${marker}`;
|
|
121
135
|
}
|
|
122
136
|
|
|
123
137
|
private activeName(): FieldName {
|
package/src/ui/pi-ui-compat.ts
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Host-compatibility shims for the `ExtensionContext["ui"]` surface.
|
|
5
|
+
*
|
|
6
|
+
* RETENTION POLICY (W4 2026-10-03): these typeof-based capability guards are
|
|
7
|
+
* deliberate no-ops, NOT version guards, and must be kept. pi-crew declares
|
|
8
|
+
* `"@earendil-works/pi-coding-agent": "*"` and `"@earendil-works/pi-tui": "*"`
|
|
9
|
+
* as optional peerDependencies with no minimum-version floor
|
|
10
|
+
* (package.json peerDependencies/peerDependenciesMeta), so host installs older
|
|
11
|
+
* than any SDK floor we verify against must keep working — retiring these
|
|
12
|
+
* shims would crash hosts whose UI predates
|
|
13
|
+
* `setWidget`/`setFooter`/`custom`/`setStatus`/`setWorkingIndicator`.
|
|
14
|
+
* Verified live: each guard is a pure feature check; there is no version
|
|
15
|
+
* constant or gate in this file. New host UI APIs should be added here
|
|
16
|
+
* (feature-detected), not called directly from src/.
|
|
17
|
+
*/
|
|
18
|
+
|
|
3
19
|
export interface WorkingIndicatorOptions {
|
|
4
20
|
frames?: string[];
|
|
5
21
|
intervalMs?: number;
|
|
@@ -9,7 +25,6 @@ type UiContext = Pick<ExtensionContext, "ui">;
|
|
|
9
25
|
type ExtensionUi = ExtensionContext["ui"];
|
|
10
26
|
type WidgetContent = string[] | ((tui: unknown, theme: unknown) => unknown);
|
|
11
27
|
type WidgetOptions = Parameters<ExtensionUi["setWidget"]>[2];
|
|
12
|
-
type WidgetOptionsWithPersist = WidgetOptions & { persist?: boolean };
|
|
13
28
|
|
|
14
29
|
type CustomOptions = Parameters<ExtensionUi["custom"]>[1];
|
|
15
30
|
|
|
@@ -38,16 +53,10 @@ export function setWorkingIndicator(ctx: UiContext, options?: WorkingIndicatorOp
|
|
|
38
53
|
|
|
39
54
|
/** Install or update an extension widget slot.
|
|
40
55
|
* No-op when the host UI predates the `setWidget` API. */
|
|
41
|
-
export function setExtensionWidget(
|
|
42
|
-
ctx: UiContext,
|
|
43
|
-
key: string,
|
|
44
|
-
content: WidgetContent | undefined,
|
|
45
|
-
options?: WidgetOptionsWithPersist,
|
|
46
|
-
): void {
|
|
47
|
-
const { persist: _persist, ...widgetOptions } = options ?? {};
|
|
56
|
+
export function setExtensionWidget(ctx: UiContext, key: string, content: WidgetContent | undefined, options?: WidgetOptions): void {
|
|
48
57
|
const record = maybeRecord(ctx.ui);
|
|
49
58
|
const fn = record?.setWidget;
|
|
50
|
-
if (typeof fn === "function") fn.call(ctx.ui, key, content as never,
|
|
59
|
+
if (typeof fn === "function") fn.call(ctx.ui, key, content as never, options);
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
/**
|
package/src/ui/rail.ts
CHANGED
|
@@ -58,7 +58,20 @@ export const RAIL = { open: "┏", section: "┣", body: "┃", close: "┗" } a
|
|
|
58
58
|
*/
|
|
59
59
|
export const PI_EXPAND_CHORD = "ctrl+o";
|
|
60
60
|
|
|
61
|
-
/** Selection cursor (lists) and active/step marker (sections, goals).
|
|
61
|
+
/** Selection cursor (lists) and active/step marker (sections, goals).
|
|
62
|
+
*
|
|
63
|
+
* R3-6 audit (2026-10-04): this glyph marks LIST SELECTION in dashboards,
|
|
64
|
+
* pickers, settings menus, and compose field rows — a navigation affordance,
|
|
65
|
+
* not a text insertion point. It therefore deliberately does NOT adopt
|
|
66
|
+
* pi-tui's `CURSOR_MARKER` (the zero-width APC sequence that anchors the
|
|
67
|
+
* IME candidate window at a TEXT cursor). Sites that DO own a text cursor —
|
|
68
|
+
* mailbox-compose field values, settings text-input/edit buffers — emit
|
|
69
|
+
* `CURSOR_MARKER` at their buffer end in addition to any visual glyph.
|
|
70
|
+
* Not converted, by design: run-dashboard run rows, settings menu rows,
|
|
71
|
+
* agents-jobs-browser rows, mailbox-detail rows, agent-picker rows,
|
|
72
|
+
* schedules-pane job rows (all list selection); widget dock `❯` (a
|
|
73
|
+
* single-line focus acknowledgement, not an input); mascot `█` frames
|
|
74
|
+
* (animation glyphs). */
|
|
62
75
|
export const CURSOR = "›";
|
|
63
76
|
export const ACTIVE = "▸";
|
|
64
77
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* label/value alignment, inline toggle, select submenu, and text input.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { CURSOR_MARKER } from "@earendil-works/pi-tui";
|
|
7
8
|
import { truncateToWidth, visibleWidth } from "../utils/visual.ts";
|
|
8
9
|
import { keyOf, matchesKey } from "./key-utils.ts";
|
|
9
10
|
import { CURSOR, canopyLine, formatHint, overflowHint, RAIL, railLine } from "./rail.ts";
|
|
@@ -601,7 +602,12 @@ class TextinputSubmenu {
|
|
|
601
602
|
lines.push(this.theme.fg("muted", this.description));
|
|
602
603
|
}
|
|
603
604
|
lines.push("");
|
|
604
|
-
|
|
605
|
+
// R3-6 (IME): the marker before the fake `█` cursor is pi-tui's
|
|
606
|
+
// zero-width CURSOR_MARKER — the host TUI strips it and positions the
|
|
607
|
+
// hardware cursor there so the IME candidate window tracks the buffer
|
|
608
|
+
// end (this submenu's buffer appends/erases at end only). Emitted
|
|
609
|
+
// unconditionally: the row renders only while this input owns keys.
|
|
610
|
+
lines.push(` ${this.buffer}${CURSOR_MARKER}█`);
|
|
605
611
|
lines.push("");
|
|
606
612
|
lines.push(
|
|
607
613
|
this.theme.fg(
|
|
@@ -730,7 +736,10 @@ class AgentOverridesSubmenu {
|
|
|
730
736
|
const lines: string[] = [];
|
|
731
737
|
lines.push(this.theme.bold(this.theme.fg("accent", `Edit ${agent} ${field}`)));
|
|
732
738
|
lines.push("");
|
|
733
|
-
|
|
739
|
+
// R3-6 (IME): same CURSOR_MARKER contract as TextinputSubmenu — the
|
|
740
|
+
// edit buffer is end-append only, so the marker rides immediately before
|
|
741
|
+
// the fake `█` cursor (pi-tui Input's marker+cursorChar pattern).
|
|
742
|
+
lines.push(` ${this.editBuffer}${CURSOR_MARKER}█`);
|
|
734
743
|
lines.push("");
|
|
735
744
|
lines.push(
|
|
736
745
|
this.theme.fg(
|
package/src/ui/widget/index.ts
CHANGED
|
@@ -522,7 +522,6 @@ export function updateCrewWidget(
|
|
|
522
522
|
const model = state.model;
|
|
523
523
|
setExtensionWidget(ctx, WIDGET_KEY, ((_tui: unknown, theme: unknown) => new CrewWidgetComponent(model, theme, _tui)) as never, {
|
|
524
524
|
placement: piPlacement,
|
|
525
|
-
persist: true,
|
|
526
525
|
});
|
|
527
526
|
state.lastVisibility = "visible";
|
|
528
527
|
state.lastPlacement = placement;
|
|
@@ -84,8 +84,15 @@ function taskStatusIcon(task: TeamTaskState): string {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
/**
|
|
88
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Plan-card elapsed dialect (QW#5): `45s` / `2m 49s` / `1h 03m`, empty
|
|
89
|
+
* string for non-finite/negative input. Deliberately NOT ui/format-helpers'
|
|
90
|
+
* canonical `formatDuration` (that one renders `12.3s`/`3m5s` with no hours
|
|
91
|
+
* bucket and no invalid-input guard) — the spaced, zero-padded, hour-capable
|
|
92
|
+
* format here is pinned by the pi-tasks plan-card display. Renamed from a
|
|
93
|
+
* local `formatDuration` to end the three-way name collision.
|
|
94
|
+
*/
|
|
95
|
+
function formatTaskElapsed(ms: number): string {
|
|
89
96
|
if (!Number.isFinite(ms) || ms < 0) return "";
|
|
90
97
|
const totalSeconds = Math.floor(ms / 1000);
|
|
91
98
|
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
@@ -110,7 +117,7 @@ function runningSuffix(task: TeamTaskState): string {
|
|
|
110
117
|
const start = Date.parse(task.startedAt ?? "");
|
|
111
118
|
if (Number.isFinite(start) && start > 0) {
|
|
112
119
|
const end = task.finishedAt ? Date.parse(task.finishedAt) : Date.now();
|
|
113
|
-
const elapsed =
|
|
120
|
+
const elapsed = formatTaskElapsed(end - start);
|
|
114
121
|
if (elapsed) parts.push(elapsed);
|
|
115
122
|
}
|
|
116
123
|
const input = task.usage?.input ?? 0;
|
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
* Extracted from crew-widget.ts.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
import * as piTui from "@earendil-works/pi-tui";
|
|
7
9
|
import { getCrewScheduler, getScheduledJobs, getScheduledJobsHiddenCountView } from "../../extension/team-tool/handle-schedule.ts";
|
|
8
10
|
import type { CrewAgentRecord } from "../../runtime/crew-agent-runtime.ts";
|
|
9
11
|
import { isPlanApprovalStatePending } from "../../runtime/plan-approval.ts";
|
|
10
12
|
import { isFinishedRunStatus } from "../../runtime/process-status.ts";
|
|
11
13
|
import type { ScheduledJob } from "../../runtime/scheduling/scheduler.ts";
|
|
14
|
+
import type { TeamRunManifest } from "../../state/types.ts";
|
|
12
15
|
import { formatRelativeTime } from "../../utils/relative-time.ts";
|
|
13
16
|
import { truncate, visibleWidth } from "../../utils/visual.ts";
|
|
14
17
|
import { Box, Text } from "../layout-primitives.ts";
|
|
@@ -95,9 +98,52 @@ export function widgetRailSlot(runs: readonly WidgetRun[]): RailSlot {
|
|
|
95
98
|
/** `<team>` (or `team/workflow` when they differ); several live runs collapse
|
|
96
99
|
* to `<n> runs` — the counts on the row stay aggregate. */
|
|
97
100
|
function dockSubject(runs: WidgetRun[]): string {
|
|
98
|
-
if (runs.length
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
+
if (runs.length === 0) return "idle";
|
|
102
|
+
const label = runs.length > 1 ? `${runs.length} runs` : shortRunLabel(runs[0]!.run);
|
|
103
|
+
// R3-9: the dock's ONE clickable action — the subject opens the LATEST
|
|
104
|
+
// run's artifacts directory (`file://` OSC-8 link). Terminals without
|
|
105
|
+
// OSC-8 support ignore the escapes and render the plain label.
|
|
106
|
+
return linkify(label, latestArtifactsUrl(runs));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── R3-9: OSC-8 dock link ─────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
type TuiHyperlink = (text: string, url: string) => string;
|
|
112
|
+
|
|
113
|
+
/** pi-tui's OSC-8 helper, resolved defensively: the peer range is `*`, so a
|
|
114
|
+
* host running a pi-tui build without the export must still render the
|
|
115
|
+
* plain dock instead of failing the namespace import. */
|
|
116
|
+
const tuiHyperlink: TuiHyperlink | undefined = (() => {
|
|
117
|
+
const candidate = (piTui as { hyperlink?: unknown }).hyperlink;
|
|
118
|
+
return typeof candidate === "function" ? (candidate as TuiHyperlink) : undefined;
|
|
119
|
+
})();
|
|
120
|
+
|
|
121
|
+
/** `file://` URL for an artifacts dir; `undefined` keeps the label plain. */
|
|
122
|
+
function artifactsDirUrl(artifactsRoot: string | undefined): string | undefined {
|
|
123
|
+
if (!artifactsRoot) return undefined;
|
|
124
|
+
try {
|
|
125
|
+
return pathToFileURL(artifactsRoot).href;
|
|
126
|
+
} catch {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Latest (by createdAt) run's artifacts dir — the dock link target. */
|
|
132
|
+
function latestArtifactsUrl(runs: readonly WidgetRun[]): string | undefined {
|
|
133
|
+
let latest: TeamRunManifest | undefined;
|
|
134
|
+
for (const entry of runs) {
|
|
135
|
+
const candidate = entry.run;
|
|
136
|
+
if (!candidate.artifactsRoot) continue;
|
|
137
|
+
if (!latest || (candidate.createdAt ?? "") > (latest.createdAt ?? "")) latest = candidate;
|
|
138
|
+
}
|
|
139
|
+
return artifactsDirUrl(latest?.artifactsRoot);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Wrap in OSC-8 when both the pi-tui helper and a target exist; plain text
|
|
143
|
+
* otherwise (clean degrade on non-hyperlink terminals and older hosts). */
|
|
144
|
+
function linkify(text: string, url: string | undefined): string {
|
|
145
|
+
if (!url || !tuiHyperlink) return text;
|
|
146
|
+
return tuiHyperlink(text, url);
|
|
101
147
|
}
|
|
102
148
|
|
|
103
149
|
/**
|
|
@@ -389,7 +435,10 @@ export function buildWidgetLines(
|
|
|
389
435
|
*/
|
|
390
436
|
export function colorWidgetLine(line: string, index: number, theme: CrewTheme, slot: RailSlot = "border"): string {
|
|
391
437
|
let result = line;
|
|
392
|
-
|
|
438
|
+
// R3-9: the guard detects THEME styling (CSI `ESC[`), not any escape — the
|
|
439
|
+
// dock's OSC-8 artifacts link (introduced by `dockSubject`) is not styling
|
|
440
|
+
// and must NOT opt the line out of the identity paint pass.
|
|
441
|
+
if (index === 0 && !result.includes("\u001b[")) {
|
|
393
442
|
// `┏|┃ <WORD> ▸ <subject>`: rail glyph takes the state slot, the identity
|
|
394
443
|
// word accent+bold, the subject toolTitle+bold. The subject is bounded by
|
|
395
444
|
// the first ` ·` so a count segment can never be swallowed.
|
|
@@ -15,11 +15,19 @@
|
|
|
15
15
|
export function formatRelativeTime(now: Date, target: Date): string {
|
|
16
16
|
const deltaMs = target.getTime() - now.getTime();
|
|
17
17
|
if (deltaMs === 0) return "now";
|
|
18
|
-
if (deltaMs > 0) return `in ${
|
|
19
|
-
return `${
|
|
18
|
+
if (deltaMs > 0) return `in ${formatRelativeCompact(deltaMs)}`;
|
|
19
|
+
return `${formatRelativeCompact(-deltaMs)} ago`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Relative-time bucket ladder (dialect D6-T4): `45s` / `84m` / `14h` /
|
|
24
|
+
* `2d14h`, zero-hour remainder collapses to `3d`. A distinct dialect from
|
|
25
|
+
* ui/format-helpers' canonical `formatDuration` (QW#5 collision resolution):
|
|
26
|
+
* the bucket boundaries (60s / 120m / 48h) are design-approved for relative
|
|
27
|
+
* timestamps and must not adopt the generic `3m5s` rendering. Formerly named
|
|
28
|
+
* `formatDurationCompact`; private to this module.
|
|
29
|
+
*/
|
|
30
|
+
function formatRelativeCompact(ms: number): string {
|
|
23
31
|
const totalSeconds = Math.floor(ms / 1000);
|
|
24
32
|
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
25
33
|
const totalMinutes = Math.floor(totalSeconds / 60);
|
package/src/utils/visual.ts
CHANGED
|
@@ -60,11 +60,41 @@ export function __test__visibleWidthCacheSize(): number {
|
|
|
60
60
|
function consumeAnsi(input: string, index: number): number {
|
|
61
61
|
const char = input[index];
|
|
62
62
|
if (!char || char !== "\u001b") return 0;
|
|
63
|
-
|
|
63
|
+
const next = input[index + 1];
|
|
64
|
+
if (next === "[") {
|
|
65
|
+
let i = index + 2;
|
|
66
|
+
while (i < input.length) {
|
|
67
|
+
const code = input.charCodeAt(i);
|
|
68
|
+
if (code >= 0x40 && code <= 0x7e) return i - index + 1;
|
|
69
|
+
i++;
|
|
70
|
+
}
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
// R3-9: OSC sequences (`ESC ] … BEL` or `ESC ] … ESC \`) — OSC-8 hyperlinks
|
|
74
|
+
// (pi-tui `hyperlink()`, used by the crew widget) ride this branch. The
|
|
75
|
+
// truncation/wrap/sanitize helpers must treat the whole sequence as ONE
|
|
76
|
+
// zero-width unit, matching pi-tui's `visibleWidth` (which already strips
|
|
77
|
+
// OSC), or a clipped line would slice mid-escape and leak raw bytes into
|
|
78
|
+
// the terminal. Malformed payloads (stray control char) fall back to the
|
|
79
|
+
// pre-existing behaviour of treating the ESC as ordinary data.
|
|
80
|
+
//
|
|
81
|
+
// U6/DR4 (pi 1.0.4 adoption, 2026-10-06): APC sequences (`ESC _ … BEL` or
|
|
82
|
+
// `ESC _ … ESC \`) share the exact same grammar, so they ride the same
|
|
83
|
+
// scanner. pi-tui's `CURSOR_MARKER` — now a documented public contract —
|
|
84
|
+
// is an APC string (`\u001b_pi:c\u0007`) that focused components emit at
|
|
85
|
+
// the cursor position. Without this branch, truncateToWidth/wrapHard
|
|
86
|
+
// walked the marker's `_pi:c` payload as ordinary text (~6 visible
|
|
87
|
+
// columns) and could slice the sequence in half; with it, the whole
|
|
88
|
+
// marker is consumed as ONE zero-width unit, matching pi-tui's
|
|
89
|
+
// `visibleWidth` (which already zero-widths APC). Same malformed fallback:
|
|
90
|
+
// a stray control char cannot belong to an OSC/APC payload either.
|
|
91
|
+
if (next !== "]" && next !== "_") return 0;
|
|
64
92
|
let i = index + 2;
|
|
65
93
|
while (i < input.length) {
|
|
66
94
|
const code = input.charCodeAt(i);
|
|
67
|
-
if (code
|
|
95
|
+
if (code === 0x07) return i - index + 1; // BEL terminator
|
|
96
|
+
if (code === 0x1b && input[i + 1] === "\\") return i - index + 2; // ST terminator
|
|
97
|
+
if (code < 0x20) return 0; // control char cannot belong to an OSC/APC payload
|
|
68
98
|
i++;
|
|
69
99
|
}
|
|
70
100
|
return 0;
|