@jwilger/pi-development-system 0.48.0 → 0.51.1
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/extensions/development-system.ts +1 -1
- package/package.json +2 -2
- package/skills/strict-lints/SKILL.md +2 -1
- package/src/core/profile.ts +3 -27
- package/src/core/spawn-overrides.ts +2 -2
- package/src/core/test-runner.ts +10 -3
- package/src/review/digest.ts +7 -1
- package/src/state/profile-detect.ts +27 -0
- package/src/subagents/VENDORED.md +1 -1
- package/src/subagents/index.ts +144 -60
- package/src/subagents/orch/inherited-tools.ts +3 -3
- package/src/subagents/orch/mailbox.ts +11 -7
- package/src/subagents/orch/manager.ts +90 -57
- package/src/subagents/orch/runtime.ts +38 -33
- package/src/subagents/orch/tools.ts +12 -10
- package/src/subagents/prefs/agent-import.ts +13 -13
- package/src/subagents/prefs/config.ts +85 -185
- package/src/subagents/prefs/import-discovery.ts +6 -6
- package/src/subagents/prefs/models.ts +6 -10
- package/src/subagents/prefs/settings.ts +38 -38
- package/src/subagents/types.ts +37 -34
- package/src/subagents/ui/agent-navigation-editor.ts +20 -13
- package/src/subagents/ui/dialog.ts +99 -84
- package/src/subagents/ui/import-picker.ts +28 -11
- package/src/subagents/ui/live-agent-view.ts +17 -5
- package/src/subagents/ui/model-picker.ts +26 -17
- package/src/subagents/ui/settings-ui.ts +16 -34
- package/src/subagents/ui/status-ui.ts +140 -51
- package/src/subagents/ui/thread-tree.ts +3 -9
- package/src/subagents/ui/ui.ts +162 -265
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
|
|
2
|
-
import { lstatSync, readdirSync, readFileSync, type
|
|
2
|
+
import { type Dirent, lstatSync, readdirSync, readFileSync, type Stats } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { delimiter, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { parseDocument } from "yaml";
|
|
@@ -17,8 +17,8 @@ export interface DiscoverImportOptions {
|
|
|
17
17
|
cwd: string;
|
|
18
18
|
agentDir: string;
|
|
19
19
|
includeProject: boolean;
|
|
20
|
-
homeDir?: string;
|
|
21
|
-
extraAgentDirs?: string;
|
|
20
|
+
homeDir?: string | undefined;
|
|
21
|
+
extraAgentDirs?: string | undefined;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const EXTRA_AGENT_DIRS_ENV = "PI_SUBAGENT_EXTRA_AGENT_DIRS";
|
|
@@ -227,7 +227,7 @@ function walk(
|
|
|
227
227
|
continue;
|
|
228
228
|
}
|
|
229
229
|
if (isSkillMarkdownName(entry.name)) continue;
|
|
230
|
-
if (!entry.isFile()
|
|
230
|
+
if (!(entry.isFile() && entry.name.endsWith(".md")) || entry.name.endsWith(".chain.md")) {
|
|
231
231
|
continue;
|
|
232
232
|
}
|
|
233
233
|
if (isExcluded(child, excludes) || isManagerOwned(child, managerDirs)) continue;
|
|
@@ -270,7 +270,7 @@ function considerFile(
|
|
|
270
270
|
const rawName = fields.name?.trim() ?? "";
|
|
271
271
|
const name = rawName || (permissive ? filenameName(resolved) : "");
|
|
272
272
|
const description = fields.description ?? (permissive ? "" : undefined);
|
|
273
|
-
if (!name || description === undefined || (
|
|
273
|
+
if (!name || description === undefined || !(permissive || description.trim())) return;
|
|
274
274
|
const packageName = fields.packageName?.trim() ?? "";
|
|
275
275
|
candidates.push({
|
|
276
276
|
id: resolved,
|
|
@@ -303,7 +303,7 @@ function looseDisplayFields(content: string): DisplayFields | undefined {
|
|
|
303
303
|
/* Ambiguous display metadata is not selectable. */
|
|
304
304
|
}
|
|
305
305
|
}
|
|
306
|
-
if (!fields.name
|
|
306
|
+
if (!(fields.name && fields.description)) return undefined;
|
|
307
307
|
return { name: fields.name, description: fields.description, packageName: fields.package };
|
|
308
308
|
}
|
|
309
309
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
|
|
2
1
|
import { resolveCandidate } from "../../core/models.ts";
|
|
3
2
|
|
|
4
3
|
const MODEL_IDENTITY = /^[^\s/]+\/[^\s]+$/;
|
|
@@ -15,15 +14,12 @@ export function getModelPreferences(type: {
|
|
|
15
14
|
}): string[] | undefined {
|
|
16
15
|
const hasModels = type.models !== undefined;
|
|
17
16
|
const hasModel = type.model !== undefined;
|
|
18
|
-
if (hasModels && hasModel)
|
|
19
|
-
throw new Error("Specify either models or model, not both");
|
|
17
|
+
if (hasModels && hasModel) throw new Error("Specify either models or model, not both");
|
|
20
18
|
if (hasModels) {
|
|
21
19
|
if (!Array.isArray(type.models))
|
|
22
20
|
throw new Error("models must be an array of provider/model-id strings");
|
|
23
21
|
if (type.models.length === 0)
|
|
24
|
-
throw new Error(
|
|
25
|
-
"models must not be empty; omit models to inherit the parent/default model",
|
|
26
|
-
);
|
|
22
|
+
throw new Error("models must not be empty; omit models to inherit the parent/default model");
|
|
27
23
|
const models = type.models.map((value, index) =>
|
|
28
24
|
validateModelPreference(value, `models[${index}]`),
|
|
29
25
|
);
|
|
@@ -40,11 +36,11 @@ export function modelIdentity(model: { provider: string; id: string }): string {
|
|
|
40
36
|
}
|
|
41
37
|
|
|
42
38
|
export class ModelPreferenceError extends Error {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
) {
|
|
39
|
+
readonly scopedModelFiltering: boolean;
|
|
40
|
+
|
|
41
|
+
constructor(message: string, scopedModelFiltering: boolean) {
|
|
47
42
|
super(message);
|
|
43
|
+
this.scopedModelFiltering = scopedModelFiltering;
|
|
48
44
|
}
|
|
49
45
|
|
|
50
46
|
get summary(): string {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
//
|
|
1
|
+
// biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
2
|
+
// biome-ignore-all lint/suspicious/noShadow: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
4
|
import {
|
|
4
5
|
closeSync,
|
|
@@ -23,11 +24,7 @@ export type WidgetMode = (typeof WIDGET_MODES)[number];
|
|
|
23
24
|
const TOOL_FILTERING_MODES = ["allowed", "all-except-blocked", "all"] as const;
|
|
24
25
|
export type ToolFilteringMode = (typeof TOOL_FILTERING_MODES)[number];
|
|
25
26
|
|
|
26
|
-
const MODEL_SELECTION_MODES = [
|
|
27
|
-
"pick-first-available",
|
|
28
|
-
"pick-first-scoped",
|
|
29
|
-
"use-current",
|
|
30
|
-
] as const;
|
|
27
|
+
const MODEL_SELECTION_MODES = ["pick-first-available", "pick-first-scoped", "use-current"] as const;
|
|
31
28
|
export type ModelSelectionMode = (typeof MODEL_SELECTION_MODES)[number];
|
|
32
29
|
|
|
33
30
|
export interface ManagerSettings {
|
|
@@ -55,7 +52,15 @@ export const DEFAULT_MANAGER_SETTINGS: ManagerSettings = {
|
|
|
55
52
|
};
|
|
56
53
|
|
|
57
54
|
const KEYS = [
|
|
58
|
-
"maxLevels",
|
|
55
|
+
"maxLevels",
|
|
56
|
+
"maxConcurrent",
|
|
57
|
+
"maxThreads",
|
|
58
|
+
"modelSelection",
|
|
59
|
+
"subagentMode",
|
|
60
|
+
"toolFiltering",
|
|
61
|
+
"widgetMode",
|
|
62
|
+
"nerdFontIcons",
|
|
63
|
+
"finalRecap",
|
|
59
64
|
] as const;
|
|
60
65
|
const MAX_LEVELS = 32;
|
|
61
66
|
|
|
@@ -65,9 +70,7 @@ function errorCode(error: unknown): string | undefined {
|
|
|
65
70
|
: undefined;
|
|
66
71
|
}
|
|
67
72
|
|
|
68
|
-
function lstatIfPresent(
|
|
69
|
-
path: string,
|
|
70
|
-
): ReturnType<typeof lstatSync> | undefined {
|
|
73
|
+
function lstatIfPresent(path: string): ReturnType<typeof lstatSync> | undefined {
|
|
71
74
|
try {
|
|
72
75
|
return lstatSync(path);
|
|
73
76
|
} catch (error) {
|
|
@@ -76,9 +79,7 @@ function lstatIfPresent(
|
|
|
76
79
|
}
|
|
77
80
|
}
|
|
78
81
|
|
|
79
|
-
function assertNotSymlinkPath(
|
|
80
|
-
path: string,
|
|
81
|
-
): ReturnType<typeof lstatSync> | undefined {
|
|
82
|
+
function assertNotSymlinkPath(path: string): ReturnType<typeof lstatSync> | undefined {
|
|
82
83
|
const stat = lstatIfPresent(path);
|
|
83
84
|
if (stat?.isSymbolicLink()) throw new Error(`Unsafe symlink path: ${path}`);
|
|
84
85
|
return stat;
|
|
@@ -107,8 +108,8 @@ function parseSettings(content: string): Partial<ManagerSettings> {
|
|
|
107
108
|
try {
|
|
108
109
|
parsed = JSON.parse(content);
|
|
109
110
|
} catch (error) {
|
|
110
|
-
const
|
|
111
|
-
throw new Error(`Invalid JSON: ${
|
|
111
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
112
|
+
throw new Error(`Invalid JSON: ${reason}`, { cause: error });
|
|
112
113
|
}
|
|
113
114
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
114
115
|
throw new Error("Settings must be a JSON object");
|
|
@@ -123,7 +124,9 @@ function parseSettings(content: string): Partial<ManagerSettings> {
|
|
|
123
124
|
if (typeof record.scopedModelFiltering !== "boolean")
|
|
124
125
|
throw new Error("scopedModelFiltering must be a boolean");
|
|
125
126
|
if (!Object.hasOwn(record, "modelSelection"))
|
|
126
|
-
layer.modelSelection = record.scopedModelFiltering
|
|
127
|
+
layer.modelSelection = record.scopedModelFiltering
|
|
128
|
+
? "pick-first-scoped"
|
|
129
|
+
: "pick-first-available";
|
|
127
130
|
}
|
|
128
131
|
for (const key of KEYS) {
|
|
129
132
|
if (!Object.hasOwn(record, key)) continue;
|
|
@@ -186,8 +189,7 @@ function readLayerContent(entry: SettingsLayer): string | undefined {
|
|
|
186
189
|
}
|
|
187
190
|
const stat = assertNotSymlinkPath(entry.filePath);
|
|
188
191
|
if (!stat) return undefined;
|
|
189
|
-
if (!stat.isFile())
|
|
190
|
-
throw new Error(`Settings must be a regular file: ${entry.filePath}`);
|
|
192
|
+
if (!stat.isFile()) throw new Error(`Settings must be a regular file: ${entry.filePath}`);
|
|
191
193
|
return readFileSync(entry.filePath, "utf8");
|
|
192
194
|
}
|
|
193
195
|
|
|
@@ -214,9 +216,13 @@ export function loadManagerSaveScope(options: {
|
|
|
214
216
|
if (content !== undefined) {
|
|
215
217
|
const state: unknown = JSON.parse(content);
|
|
216
218
|
if (
|
|
217
|
-
state &&
|
|
218
|
-
|
|
219
|
-
|
|
219
|
+
state &&
|
|
220
|
+
typeof state === "object" &&
|
|
221
|
+
!Array.isArray(state) &&
|
|
222
|
+
"saveScope" in state &&
|
|
223
|
+
state.saveScope === "user"
|
|
224
|
+
)
|
|
225
|
+
return "user";
|
|
220
226
|
}
|
|
221
227
|
} catch {
|
|
222
228
|
// Missing, invalid or unsafe UI state must not prevent opening settings.
|
|
@@ -243,8 +249,7 @@ export function loadManagerSettings(options: {
|
|
|
243
249
|
const diagnostics: string[] = [];
|
|
244
250
|
const settings: ManagerSettings = { ...DEFAULT_MANAGER_SETTINGS };
|
|
245
251
|
const layers = [layer(options.agentDir, ["subagent-manager"])];
|
|
246
|
-
if (options.includeProject)
|
|
247
|
-
layers.push(layer(options.cwd, [".pi", "agent", "subagent-manager"]));
|
|
252
|
+
if (options.includeProject) layers.push(layer(options.cwd, [".pi", "agent", "subagent-manager"]));
|
|
248
253
|
for (const entry of layers) {
|
|
249
254
|
try {
|
|
250
255
|
const parsed = readLayer(entry);
|
|
@@ -257,13 +262,6 @@ export function loadManagerSettings(options: {
|
|
|
257
262
|
}
|
|
258
263
|
|
|
259
264
|
function serializedSettings(settings: ManagerSettings): string {
|
|
260
|
-
if (
|
|
261
|
-
settings === null ||
|
|
262
|
-
typeof settings !== "object" ||
|
|
263
|
-
Array.isArray(settings)
|
|
264
|
-
) {
|
|
265
|
-
throw new Error("Settings must be a JSON object");
|
|
266
|
-
}
|
|
267
265
|
for (const key of Object.keys(settings)) {
|
|
268
266
|
if (!isSupportedKey(key)) throw new Error(`Unknown settings key: ${key}`);
|
|
269
267
|
}
|
|
@@ -298,17 +296,20 @@ function ensureOwnedDirectory(directory: string): void {
|
|
|
298
296
|
}
|
|
299
297
|
|
|
300
298
|
function ensureScopeDirectories(directories: string[]): void {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
299
|
+
const [root, ...rest] = directories;
|
|
300
|
+
if (root === undefined) return;
|
|
301
|
+
assertExistingDirectory(root);
|
|
302
|
+
let parent = root;
|
|
303
|
+
for (const directory of rest) {
|
|
304
|
+
assertExistingDirectory(parent);
|
|
305
|
+
ensureOwnedDirectory(directory);
|
|
306
|
+
parent = directory;
|
|
305
307
|
}
|
|
306
308
|
}
|
|
307
309
|
|
|
308
310
|
function assertRegularFileOrAbsent(filePath: string): void {
|
|
309
311
|
const stat = assertNotSymlinkPath(filePath);
|
|
310
|
-
if (stat && !stat.isFile())
|
|
311
|
-
throw new Error(`Settings must be a regular file: ${filePath}`);
|
|
312
|
+
if (stat && !stat.isFile()) throw new Error(`Settings must be a regular file: ${filePath}`);
|
|
312
313
|
}
|
|
313
314
|
|
|
314
315
|
function exclusiveCreateFlags(): number {
|
|
@@ -333,8 +334,7 @@ function writeAtomically(entry: SettingsLayer, content: string): void {
|
|
|
333
334
|
for (const directory of entry.directories) assertExistingDirectory(directory);
|
|
334
335
|
assertRegularFileOrAbsent(filePath);
|
|
335
336
|
const tempStat = assertNotSymlinkPath(temporary);
|
|
336
|
-
if (!tempStat?.isFile())
|
|
337
|
-
throw new Error(`Settings must be a regular file: ${temporary}`);
|
|
337
|
+
if (!tempStat?.isFile()) throw new Error(`Settings must be a regular file: ${temporary}`);
|
|
338
338
|
renameSync(temporary, filePath);
|
|
339
339
|
} finally {
|
|
340
340
|
if (created) {
|
package/src/subagents/types.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
|
|
2
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
3
2
|
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
4
3
|
|
|
@@ -11,22 +10,24 @@ export interface ResolvedAgentSettings {
|
|
|
11
10
|
export interface AgentType {
|
|
12
11
|
name: string;
|
|
13
12
|
description: string;
|
|
14
|
-
models?: string[];
|
|
13
|
+
models?: string[] | undefined;
|
|
15
14
|
/** @deprecated Use ordered models instead. */
|
|
16
|
-
model?: string;
|
|
15
|
+
model?: string | undefined;
|
|
17
16
|
/** Advisory display names. Not provider/model pins and never used for selection. */
|
|
18
|
-
modelSuggestions?: string[];
|
|
19
|
-
thinkingLevel?: ThinkingLevel;
|
|
17
|
+
modelSuggestions?: string[] | undefined;
|
|
18
|
+
thinkingLevel?: ThinkingLevel | undefined;
|
|
20
19
|
/** devsys: per-spawn pin set by the coordinator via agent_spawn; beats preferences and inheritance. */
|
|
21
|
-
spawnOverrides?:
|
|
20
|
+
spawnOverrides?:
|
|
21
|
+
| { model?: string | undefined; thinkingLevel?: ThinkingLevel | undefined }
|
|
22
|
+
| undefined;
|
|
22
23
|
/** Pi semantic token for the type pill background and task path foreground. */
|
|
23
|
-
color?: string;
|
|
24
|
+
color?: string | undefined | undefined;
|
|
24
25
|
/** Optional literal Nerd Font glyph; displayed only when the labs setting is enabled. */
|
|
25
|
-
icon?: string;
|
|
26
|
-
tools?: { allow?: string[]; block?: string[] };
|
|
26
|
+
icon?: string | undefined | undefined;
|
|
27
|
+
tools?: { allow?: string[] | undefined; block?: string[] | undefined } | undefined;
|
|
27
28
|
systemPrompt: string;
|
|
28
|
-
filePath?: string;
|
|
29
|
-
source?: "bundled" | "user" | "project";
|
|
29
|
+
filePath?: string | undefined;
|
|
30
|
+
source?: "bundled" | "user" | "project" | undefined;
|
|
30
31
|
/**
|
|
31
32
|
* How this definition is customized relative to its base.
|
|
32
33
|
* - fork: a full `<name>.md` copy (owns the system prompt).
|
|
@@ -34,15 +35,17 @@ export interface AgentType {
|
|
|
34
35
|
* (no Markdown body; unset fields follow the base).
|
|
35
36
|
* Absent for pure bundled definitions and brand-new drafts.
|
|
36
37
|
*/
|
|
37
|
-
customization?:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
customization?:
|
|
39
|
+
| {
|
|
40
|
+
kind: "fork" | "override";
|
|
41
|
+
scope: "user" | "project";
|
|
42
|
+
filePath: string;
|
|
43
|
+
}
|
|
44
|
+
| undefined;
|
|
42
45
|
/** Source of the base definition under the customization, if any. */
|
|
43
|
-
baseSource?: "bundled" | "user" | "project";
|
|
46
|
+
baseSource?: "bundled" | "user" | "project" | undefined;
|
|
44
47
|
/** File path of the base definition under the customization, if any. */
|
|
45
|
-
baseFilePath?: string;
|
|
48
|
+
baseFilePath?: string | undefined;
|
|
46
49
|
}
|
|
47
50
|
type ThreadState = "starting" | "running" | "paused" | "completed" | "failed" | "stopped";
|
|
48
51
|
export interface ThreadView {
|
|
@@ -50,33 +53,33 @@ export interface ThreadView {
|
|
|
50
53
|
parent: string | null;
|
|
51
54
|
owner: string;
|
|
52
55
|
type: string;
|
|
53
|
-
color?: string;
|
|
54
|
-
icon?: string;
|
|
56
|
+
color?: string | undefined;
|
|
57
|
+
icon?: string | undefined;
|
|
55
58
|
state: ThreadState;
|
|
56
59
|
task: string;
|
|
57
60
|
status: string;
|
|
58
|
-
output?: string;
|
|
59
|
-
error?: string;
|
|
61
|
+
output?: string | undefined;
|
|
62
|
+
error?: string | undefined;
|
|
60
63
|
createdAt: number;
|
|
61
64
|
updatedAt: number;
|
|
62
|
-
sessionFile?: string;
|
|
65
|
+
sessionFile?: string | undefined;
|
|
63
66
|
/** Frozen active time. Renderer adds `Date.now() - startedAt` while a run is active. */
|
|
64
67
|
elapsedMs?: number;
|
|
65
68
|
/** Current run start. Live only; omitted from saved views so reloads do not count offline time. */
|
|
66
|
-
startedAt?: number;
|
|
69
|
+
startedAt?: number | undefined;
|
|
67
70
|
/** Cumulative input tokens, including cache read/write. Live views include the in-progress message. */
|
|
68
71
|
inputTokens?: number;
|
|
69
72
|
/** Cumulative output tokens. Live views include the in-progress message. */
|
|
70
73
|
outputTokens?: number;
|
|
71
74
|
/** devsys: per-spawn model/thinkingLevel pin requested through agent_spawn, when any. */
|
|
72
|
-
pinned?: { model?: string; thinkingLevel?: ThinkingLevel };
|
|
75
|
+
pinned?: { model?: string | undefined; thinkingLevel?: ThinkingLevel | undefined };
|
|
73
76
|
}
|
|
74
77
|
type DriverEvent =
|
|
75
78
|
| { kind: "activity" | "error"; text: string }
|
|
76
79
|
| {
|
|
77
80
|
kind: "checkpoint";
|
|
78
81
|
text: string;
|
|
79
|
-
sessionFile?: string;
|
|
82
|
+
sessionFile?: string | undefined;
|
|
80
83
|
sessionLeafId?: string | null;
|
|
81
84
|
}
|
|
82
85
|
| {
|
|
@@ -106,7 +109,7 @@ export interface TranscriptSnapshot {
|
|
|
106
109
|
tools: readonly TranscriptToolState[];
|
|
107
110
|
inheritedCount: number;
|
|
108
111
|
thread?: ThreadView;
|
|
109
|
-
error?: string;
|
|
112
|
+
error?: string | undefined;
|
|
110
113
|
}
|
|
111
114
|
export interface TranscriptObservation {
|
|
112
115
|
snapshot: TranscriptSnapshot;
|
|
@@ -123,7 +126,7 @@ export interface AgentDriver {
|
|
|
123
126
|
abort(): Promise<void>;
|
|
124
127
|
dispose(): void;
|
|
125
128
|
sendUpdate(content: string): Promise<void> | void;
|
|
126
|
-
sessionFile?: string;
|
|
129
|
+
sessionFile?: string | undefined;
|
|
127
130
|
sessionLeafId?: string | null;
|
|
128
131
|
}
|
|
129
132
|
export interface DriverOptions {
|
|
@@ -134,7 +137,7 @@ export interface DriverOptions {
|
|
|
134
137
|
onEvent(event: DriverEvent): void;
|
|
135
138
|
shouldPause(): boolean;
|
|
136
139
|
parentPath: string | null;
|
|
137
|
-
sessionFile?: string;
|
|
140
|
+
sessionFile?: string | undefined;
|
|
138
141
|
/** Retained transcript of a nested lexical parent. Absent for /root and independent roots. */
|
|
139
142
|
parentSessionFile?: string;
|
|
140
143
|
sessionLeafId?: string | null;
|
|
@@ -145,15 +148,15 @@ export interface SavedThreadView {
|
|
|
145
148
|
parent: string | null;
|
|
146
149
|
owner: string;
|
|
147
150
|
type: string;
|
|
148
|
-
color?: string;
|
|
149
|
-
icon?: string;
|
|
151
|
+
color?: string | undefined;
|
|
152
|
+
icon?: string | undefined;
|
|
150
153
|
state: ThreadState;
|
|
151
154
|
task: string;
|
|
152
155
|
status: string;
|
|
153
|
-
output?: string;
|
|
154
|
-
error?: string;
|
|
156
|
+
output?: string | undefined;
|
|
157
|
+
error?: string | undefined;
|
|
155
158
|
createdAt: number;
|
|
156
|
-
sessionFile?: string;
|
|
159
|
+
sessionFile?: string | undefined;
|
|
157
160
|
sessionLeafId?: string | null;
|
|
158
161
|
/** Frozen cumulative active time. `startedAt` is intentionally not persisted. */
|
|
159
162
|
elapsedMs?: number;
|
|
@@ -5,11 +5,11 @@ import {
|
|
|
5
5
|
type KeybindingsManager,
|
|
6
6
|
} from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import {
|
|
8
|
+
type EditorTheme,
|
|
8
9
|
getKeybindings,
|
|
9
10
|
Key,
|
|
10
|
-
matchesKey,
|
|
11
|
-
type EditorTheme,
|
|
12
11
|
type Keybinding,
|
|
12
|
+
matchesKey,
|
|
13
13
|
type TUI,
|
|
14
14
|
} from "@earendil-works/pi-tui";
|
|
15
15
|
|
|
@@ -87,8 +87,11 @@ export class AgentNavigationEditor extends CustomEditor {
|
|
|
87
87
|
|
|
88
88
|
override handleInput(data: string): void {
|
|
89
89
|
const outer = this.dispatchDepth === 0;
|
|
90
|
-
const direction = this.ordinaryArrow(data, "left")
|
|
91
|
-
|
|
90
|
+
const direction = this.ordinaryArrow(data, "left")
|
|
91
|
+
? "left"
|
|
92
|
+
: this.ordinaryArrow(data, "right")
|
|
93
|
+
? "right"
|
|
94
|
+
: undefined;
|
|
92
95
|
if (outer && (!direction || (this.pending && this.pending !== direction))) this.inputEpoch++;
|
|
93
96
|
const previousPaste = this.pastePending;
|
|
94
97
|
const wasPaste = previousPaste || data.includes("\x1b[200~");
|
|
@@ -100,10 +103,12 @@ export class AgentNavigationEditor extends CustomEditor {
|
|
|
100
103
|
const cursor = this.getCursor();
|
|
101
104
|
const text = this.getText();
|
|
102
105
|
const lines = text.split("\n");
|
|
103
|
-
const atBoundary =
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
const atBoundary =
|
|
107
|
+
direction === "left"
|
|
108
|
+
? cursor.line === 0 && cursor.col === 0
|
|
109
|
+
: direction === "right" &&
|
|
110
|
+
cursor.line === lines.length - 1 &&
|
|
111
|
+
cursor.col === lines.at(-1)!.length;
|
|
107
112
|
const eligible =
|
|
108
113
|
outer &&
|
|
109
114
|
direction !== undefined &&
|
|
@@ -170,9 +175,10 @@ export class AgentNavigationEditor extends CustomEditor {
|
|
|
170
175
|
generation: unknown,
|
|
171
176
|
direction: "left" | "right",
|
|
172
177
|
): void {
|
|
173
|
-
const allowed = () =>
|
|
174
|
-
|
|
175
|
-
|
|
178
|
+
const allowed = () =>
|
|
179
|
+
direction === "left"
|
|
180
|
+
? this.navigation.canOpen()
|
|
181
|
+
: !!this.navigation.collapseWidget && (this.navigation.canCollapse?.() ?? false);
|
|
176
182
|
if (this.pending || !allowed()) return;
|
|
177
183
|
this.pending = direction;
|
|
178
184
|
const epoch = this.inputEpoch;
|
|
@@ -195,8 +201,9 @@ export class AgentNavigationEditor extends CustomEditor {
|
|
|
195
201
|
try {
|
|
196
202
|
// Call within the checked microtask: an extra Promise.then would leave
|
|
197
203
|
// a gap in which root ownership could change after validation.
|
|
198
|
-
void Promise.resolve(
|
|
199
|
-
? this.navigation.openTree() : this.navigation.collapseWidget!()
|
|
204
|
+
void Promise.resolve(
|
|
205
|
+
direction === "left" ? this.navigation.openTree() : this.navigation.collapseWidget!(),
|
|
206
|
+
)
|
|
200
207
|
.catch((error: unknown) => this.navigation.onError?.(error))
|
|
201
208
|
.finally(() => {
|
|
202
209
|
this.pending = undefined;
|