oira666_pi-subagent 0.5.3 → 0.5.4

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 CHANGED
@@ -111,9 +111,9 @@ Collapsed results show each child's name, task, status, and most recent activity
111
111
  | T | Current turn's tool list |
112
112
  | Up / Down, Enter | Select and open a tool or nested child |
113
113
  | C | Children across all turns |
114
- | Esc | Return to the parent view |
114
+ | Esc | Back one level: tool detail, tools/children, parent subagent, then the searchable list. From the list or a directly named subagent, close. |
115
115
  | /, n, N | Search, next match, previous match |
116
- | q | Close |
116
+ | q | Close from any transcript view |
117
117
 
118
118
  While workers run, steering input can be broadcast to selected names, including nested paths such as `John > Maria`. Only inputs marked as streaming `steer` open the routing prompt. Idle prompts and queued follow-ups remain with the parent.
119
119
 
@@ -63,6 +63,7 @@ Optionally let TypeSafe's Jev choose a model for each subagent task. Add this se
63
63
  "smart-decision": {
64
64
  "enabled": true,
65
65
  "fallback": true,
66
+ "timeout_seconds": 1200,
66
67
  "model": "jev",
67
68
  "provider_url": "https://api.typesafe.ai",
68
69
  "api_key": "YOUR_TYPESAFE_API_KEY",
@@ -80,9 +81,10 @@ Replace the example choices with models available in your Pi installation. Each
80
81
 
81
82
  - `enabled` must be `true`. Missing settings, disabled settings, or an empty model list leave existing behavior unchanged and make no Jev requests.
82
83
  - `model` selects the decision model, not the worker model. `jev` maps to TypeSafe's `jev-latest`; explicit TypeSafe model IDs also work.
84
+ - `timeout_seconds` defaults to `1200` seconds and bounds the whole Jev request, including reading the response body. Set a number from `0.001` to `2147483.647` seconds. Invalid values follow the `fallback` policy.
83
85
  - `provider_url` is the provider base URL, defaulting to `https://api.typesafe.ai`. For a local compatible server, use `http://localhost:8765`. The extension appends `/v1/systemone`; trailing slashes are ignored. Use an HTTP or HTTPS URL without credentials, query parameters, or a fragment.
84
86
  - `api_key` is your TypeSafe API key. Worker providers still need their own Pi credentials.
85
- - `fallback` defaults to `true`. When a loaded `smart-decision` object omits it, the extension adds `"fallback": true` and saves that configuration file at runtime. Existing values, including `false`, are left untouched. Untrusted project files are never changed. If saving fails, a warning is emitted and the runtime default still applies. If selection fails, a short warning is written and the worker uses the original launch settings. Set it to `false` to return a tool error without launching the affected worker. This covers network errors, the 10-second request timeout, API errors, invalid answers, and invalid enabled configuration. Cancellation cancels the task instead of falling back.
87
+ - `fallback` defaults to `true`. When a loaded `smart-decision` object omits it, the extension adds `"fallback": true` and saves that configuration file at runtime. Existing values, including `false`, are left untouched. Untrusted project files are never changed. If saving fails, a warning is emitted and the runtime default still applies. If selection fails, a short warning is written and the worker uses the original launch settings. Set it to `false` to return a tool error without launching the affected worker. This covers network errors, the configured request timeout, API errors, invalid answers, and invalid enabled configuration. Cancellation cancels the task instead of falling back.
86
88
 
87
89
  Each fresh launch and actual resume sends the agent definition's system prompt, the assigned task, and the candidate descriptions to `<provider_url>/v1/systemone`. Jev must select a configured candidate. When a task refers to a separate task-description or handoff file whose contents were not supplied, Jev is instructed to assume higher complexity and prefer a more capable model or higher reasoning level. It does not read that file. Mentioning a source filename alone does not trigger this guidance. Its provider, model, and thinking level override the parent model and agent frontmatter for that run. The parent's explicit `--provider` and `--api-key` are not forwarded when a Jev choice is applied. Startup retries reuse the choice. Finished results reused without launching a process make no request.
88
90
 
@@ -1,6 +1,7 @@
1
1
  import { buildSubagentDetail, findNameRecord, type SubagentDetail } from "../detail.js";
2
2
  import { readNamesRegistry, type NamesRegistry, type SubagentNameRecord } from "../names.js";
3
- import { SubagentPager, SubagentPicker, filterPickerItems, type PickerItem } from "../overlay.js";
3
+ import { filterPickerItems, type PickerItem } from "../overlay.js";
4
+ import { SubagentExpandView } from "../ui/expand-view.js";
4
5
  import type { SessionContext } from "./contracts.js";
5
6
  import type { ExtensionState } from "./state.js";
6
7
 
@@ -66,63 +67,43 @@ export function registerSubagentExpandCommand(state: ExtensionState): void {
66
67
  return;
67
68
  }
68
69
 
69
- let record = name ? findNameRecord(registry, name) : undefined;
70
- if (!record) {
71
- if (name) {
72
- const suggestions = filterPickerItems(
73
- sortedRecords.map((item) => ({ name: item.name, agent: item.agent, task: item.task })),
74
- name,
75
- )
76
- .slice(0, 10)
77
- .map((item) => item.name);
70
+ const record = name ? findNameRecord(registry, name) : undefined;
71
+ if (!record && name) {
72
+ const suggestions = filterPickerItems(
73
+ sortedRecords.map((item) => ({ name: item.name, agent: item.agent, task: item.task })),
74
+ name,
75
+ )
76
+ .slice(0, 10)
77
+ .map((item) => item.name);
78
+ ctx.ui.notify(
79
+ `Unknown subagent "${name}".${suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}?` : ""}`,
80
+ "error",
81
+ );
82
+ return;
83
+ }
84
+
85
+ const resolveDetail = (selectedName: string): SubagentDetail | undefined => {
86
+ const selected = findNameRecord(registry, selectedName);
87
+ if (!selected) return undefined;
88
+ try {
89
+ return buildSubagentDetail(selected, { sessionDir: resolveDetailSessionDir(state, selected) });
90
+ } catch (err) {
78
91
  ctx.ui.notify(
79
- `Unknown subagent "${name}".${suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}?` : ""}`,
92
+ `Failed to read subagent session: ${err instanceof Error ? err.message : String(err)}`,
80
93
  "error",
81
94
  );
82
- return;
95
+ return undefined;
83
96
  }
84
- // A delegation tree can hold hundreds of names, so the chooser is a
85
- // searchable overlay rather than a flat select list.
86
- const picked = await ctx.ui.custom(
87
- (tui, theme, _keybindings, done: (value: string | undefined) => void) =>
88
- new SubagentPicker({
89
- items: sortedRecords.map((item) => ({ name: item.name, agent: item.agent, task: item.task })),
90
- getRows: () => tui?.terminal?.rows ?? 30,
91
- theme,
92
- requestRender: () => tui?.requestRender?.(),
93
- onPick: (value) => done(value),
94
- }),
95
- {
96
- overlay: true,
97
- overlayOptions: { anchor: "center", width: "80%", maxHeight: "90%", margin: 1 },
98
- },
99
- );
100
- if (!picked) return;
101
- record = findNameRecord(registry, picked);
102
- if (!record) return;
103
- }
104
-
105
- let detail: SubagentDetail;
106
- try {
107
- detail = buildSubagentDetail(record, { sessionDir: resolveDetailSessionDir(state, record) });
108
- } catch (err) {
109
- ctx.ui.notify(`Failed to read subagent session: ${err instanceof Error ? err.message : String(err)}`, "error");
110
- return;
111
- }
97
+ };
98
+ const detail = record ? resolveDetail(record.name) : undefined;
99
+ if (record && !detail) return;
112
100
 
113
101
  await ctx.ui.custom(
114
102
  (tui, theme, _keybindings, done: (value: void) => void) =>
115
- new SubagentPager({
103
+ new SubagentExpandView({
104
+ items: sortedRecords.map((item) => ({ name: item.name, agent: item.agent, task: item.task })),
116
105
  detail,
117
- resolveDetail: (childName) => {
118
- const childRecord = findNameRecord(registry, childName);
119
- if (!childRecord) return undefined;
120
- try {
121
- return buildSubagentDetail(childRecord, { sessionDir: resolveDetailSessionDir(state, childRecord) });
122
- } catch {
123
- return undefined;
124
- }
125
- },
106
+ resolveDetail,
126
107
  getRows: () => tui?.terminal?.rows ?? 30,
127
108
  theme,
128
109
  requestRender: () => tui?.requestRender?.(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oira666_pi-subagent",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "Subagent extension for Pi coding agent. Delegate tasks to specialized agents.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -2,6 +2,7 @@ type Selection = { provider: string; model: string; thinking: string };
2
2
 
3
3
  export interface SmartDecisionConfig {
4
4
  readonly fallback: boolean;
5
+ readonly timeoutSeconds: number;
5
6
  readonly invalid?: boolean;
6
7
  readonly model: string;
7
8
  readonly apiKey: string;
@@ -59,6 +60,7 @@ export function parseSmartDecisionConfig(value: unknown): SmartDecisionConfig |
59
60
  const invalid = (): SmartDecisionConfig => ({
60
61
  fallback: value.fallback !== false,
61
62
  invalid: true,
63
+ timeoutSeconds: 1200,
62
64
  model: "",
63
65
  apiKey: "",
64
66
  providerUrl: DEFAULT_PROVIDER_URL,
@@ -67,6 +69,14 @@ export function parseSmartDecisionConfig(value: unknown): SmartDecisionConfig |
67
69
  if (!nonempty(value.model) || !nonempty(value.api_key) || !Array.isArray(value.use_models)) return invalid();
68
70
  const providerUrl = parseProviderUrl(value.provider_url);
69
71
  if (!providerUrl) return invalid();
72
+ const timeoutSeconds = value.timeout_seconds === undefined ? 1200 : value.timeout_seconds;
73
+ if (
74
+ typeof timeoutSeconds !== "number" ||
75
+ !Number.isFinite(timeoutSeconds) ||
76
+ timeoutSeconds < 0.001 ||
77
+ timeoutSeconds > 2_147_483.647
78
+ )
79
+ return invalid();
70
80
  const candidates: (Selection & { key: string; description: string })[] = [];
71
81
  const seen = new Set<string>();
72
82
  let count = 0;
@@ -86,6 +96,7 @@ export function parseSmartDecisionConfig(value: unknown): SmartDecisionConfig |
86
96
  const model = value.model.trim();
87
97
  return {
88
98
  fallback: value.fallback !== false,
99
+ timeoutSeconds,
89
100
  model: model === "jev" ? "jev-latest" : model,
90
101
  apiKey: value.api_key.trim(),
91
102
  providerUrl,
@@ -109,7 +120,7 @@ export async function selectSmartDecision(
109
120
  });
110
121
  controller.signal.addEventListener("abort", rejectAborted, { once: true });
111
122
  signal?.addEventListener("abort", cancel, { once: true });
112
- const timer = setTimeout(cancel, 10_000);
123
+ const timer = setTimeout(cancel, config.timeoutSeconds * 1000);
113
124
 
114
125
  try {
115
126
  if (config.invalid || !config.candidates.length) throw new Error("Invalid smart-decision settings");
@@ -0,0 +1,62 @@
1
+ import type { DetailTheme, SubagentDetail } from "../detail.js";
2
+ import { SubagentPager } from "./pager.js";
3
+ import { SubagentPicker, type PickerItem } from "./picker.js";
4
+
5
+ export interface ExpandViewOptions {
6
+ items: PickerItem[];
7
+ detail?: SubagentDetail;
8
+ resolveDetail: (name: string) => SubagentDetail | undefined;
9
+ getRows: () => number;
10
+ theme: DetailTheme;
11
+ requestRender: () => void;
12
+ onClose: () => void;
13
+ }
14
+
15
+ /** Keep the picker alive while browsing transcripts so Esc restores its search and selection. */
16
+ export class SubagentExpandView {
17
+ private readonly picker?: SubagentPicker;
18
+ private pager?: SubagentPager;
19
+
20
+ constructor(private readonly options: ExpandViewOptions) {
21
+ if (options.detail) {
22
+ this.openDetail(options.detail);
23
+ } else {
24
+ this.picker = new SubagentPicker({
25
+ ...options,
26
+ onPick: (name) => {
27
+ if (!name) options.onClose();
28
+ else {
29
+ const detail = options.resolveDetail(name);
30
+ if (detail) this.openDetail(detail);
31
+ }
32
+ },
33
+ });
34
+ }
35
+ }
36
+
37
+ private openDetail(detail: SubagentDetail): void {
38
+ this.pager = new SubagentPager({
39
+ ...this.options,
40
+ detail,
41
+ onBack: this.picker
42
+ ? () => {
43
+ this.pager = undefined;
44
+ }
45
+ : undefined,
46
+ });
47
+ this.options.requestRender();
48
+ }
49
+
50
+ handleInput(data: string): void {
51
+ (this.pager ?? this.picker)?.handleInput(data);
52
+ }
53
+
54
+ render(width: number): string[] {
55
+ return (this.pager ?? this.picker)?.render(width) ?? [];
56
+ }
57
+
58
+ invalidate(): void {
59
+ this.picker?.invalidate();
60
+ this.pager?.invalidate();
61
+ }
62
+ }
package/ui/pager-view.ts CHANGED
@@ -23,6 +23,7 @@ export interface PagerOptions {
23
23
  theme: DetailTheme;
24
24
  requestRender: () => void;
25
25
  onClose: () => void;
26
+ onBack?: () => void;
26
27
  }
27
28
 
28
29
  export class PagerView {
@@ -190,8 +191,8 @@ export class PagerView {
190
191
  if (this.status) return `${this.status} n/N next/previous • / new search`;
191
192
  const position = `${this.scroll + 1}-${Math.min(this.lines.length, this.scroll + this.viewport)}/${this.lines.length}`;
192
193
  if (this.mode === "turn") {
193
- const back = this.parentStack.length > 0 ? "Esc parent" : "Esc close";
194
- return `${position} ←/→ turn • T tools • C children • / search • ${back}`;
194
+ const back = this.parentStack.length > 0 ? "Esc parent" : this.options.onBack ? "Esc list" : "Esc close";
195
+ return `${position} ←/→ turn • T tools • C children • / search • ${back} • q close`;
195
196
  }
196
197
  if (this.mode === "tools")
197
198
  return `${position} ↑/↓ row • Enter inspect/expand child • / search • Esc back • q close`;
package/ui/pager.ts CHANGED
@@ -28,7 +28,7 @@ export class SubagentPager extends PagerView {
28
28
  this.childIndex = 0;
29
29
  this.resetView();
30
30
  } else {
31
- this.options.onClose();
31
+ (this.options.onBack ?? this.options.onClose)();
32
32
  }
33
33
  this.options.requestRender();
34
34
  return;