@tintinweb/pi-subagents 0.9.1 → 0.10.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/CHANGELOG.md +25 -0
- package/README.md +47 -15
- package/dist/agent-runner.d.ts +49 -0
- package/dist/agent-runner.js +225 -35
- package/dist/agent-types.d.ts +8 -1
- package/dist/agent-types.js +15 -4
- package/dist/custom-agents.js +21 -1
- package/dist/index.js +13 -17
- package/dist/prompts.d.ts +6 -3
- package/dist/prompts.js +12 -4
- package/dist/status-note.d.ts +13 -0
- package/dist/status-note.js +24 -0
- package/dist/types.d.ts +3 -0
- package/dist/ui/agent-widget.d.ts +4 -4
- package/dist/ui/agent-widget.js +6 -6
- package/dist/ui/conversation-viewer.d.ts +9 -1
- package/dist/ui/conversation-viewer.js +35 -2
- package/package.json +2 -1
- package/src/agent-runner.ts +238 -34
- package/src/agent-types.ts +15 -4
- package/src/custom-agents.ts +23 -1
- package/src/index.ts +13 -18
- package/src/prompts.ts +12 -4
- package/src/status-note.ts +25 -0
- package/src/types.ts +3 -0
- package/src/ui/agent-widget.ts +6 -6
- package/src/ui/conversation-viewer.ts +32 -1
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import { defineTool, getAgentDir, getSettingsListTheme } from "@earendil-works/p
|
|
|
15
15
|
import { Container, Key, matchesKey, SettingsList, Spacer, Text } from "@earendil-works/pi-tui";
|
|
16
16
|
import { Type } from "@sinclair/typebox";
|
|
17
17
|
import { AgentManager } from "./agent-manager.js";
|
|
18
|
-
import { getAgentConversation, getDefaultMaxTurns, getGraceTurns, normalizeMaxTurns, setDefaultMaxTurns, setGraceTurns, steerAgent } from "./agent-runner.js";
|
|
18
|
+
import { getAgentConversation, getDefaultMaxTurns, getGraceTurns, normalizeMaxTurns, SUBAGENT_TOOL_NAMES, setDefaultMaxTurns, setGraceTurns, steerAgent } from "./agent-runner.js";
|
|
19
19
|
import { BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, getDefaultAgentNames, getUserAgentNames, registerAgents, resolveType } from "./agent-types.js";
|
|
20
20
|
import { registerRpcHandlers } from "./cross-extension-rpc.js";
|
|
21
21
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
@@ -27,6 +27,7 @@ import { createOutputFilePath, streamToOutputFile, writeInitialEntry } from "./o
|
|
|
27
27
|
import { SubagentScheduler } from "./schedule.js";
|
|
28
28
|
import { resolveStorePath, ScheduleStore } from "./schedule-store.js";
|
|
29
29
|
import { applyAndEmitLoaded, saveAndEmitChanged } from "./settings.js";
|
|
30
|
+
import { getStatusNote } from "./status-note.js";
|
|
30
31
|
import { AgentWidget, buildInvocationTags, describeActivity, formatDuration, formatMs, formatTokens, formatTurns, getDisplayName, getPromptModeLabel, SPINNER, } from "./ui/agent-widget.js";
|
|
31
32
|
import { showSchedulesMenu } from "./ui/schedule-menu.js";
|
|
32
33
|
import { addUsage, getLifetimeTotal, getSessionContextPercent } from "./usage.js";
|
|
@@ -98,15 +99,6 @@ function getStatusLabel(status, error) {
|
|
|
98
99
|
default: return "Done";
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
|
-
/** Parenthetical status note for completed agent result text. */
|
|
102
|
-
function getStatusNote(status) {
|
|
103
|
-
switch (status) {
|
|
104
|
-
case "aborted": return " (aborted — max turns exceeded, output may be incomplete)";
|
|
105
|
-
case "steered": return " (wrapped up — reached turn limit)";
|
|
106
|
-
case "stopped": return " (stopped by user)";
|
|
107
|
-
default: return "";
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
102
|
/** Escape XML special characters to prevent injection in structured notifications. */
|
|
111
103
|
function escapeXml(s) {
|
|
112
104
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -130,7 +122,7 @@ function formatTaskNotification(record, resultMaxLen) {
|
|
|
130
122
|
record.toolCallId ? `<tool-use-id>${escapeXml(record.toolCallId)}</tool-use-id>` : null,
|
|
131
123
|
record.outputFile ? `<output-file>${escapeXml(record.outputFile)}</output-file>` : null,
|
|
132
124
|
`<status>${escapeXml(status)}</status>`,
|
|
133
|
-
`<summary>Agent "${escapeXml(record.description)}" ${record.status}</summary>`,
|
|
125
|
+
`<summary>Agent "${escapeXml(record.description)}" ${record.status}${getStatusNote(record.status)}</summary>`,
|
|
134
126
|
`<result>${escapeXml(resultPreview)}</result>`,
|
|
135
127
|
`<usage><total_tokens>${totalTokens}</total_tokens><tool_uses>${record.toolUses}</tool_uses>${ctxXml}${compactXml}<duration_ms>${durationMs}</duration_ms></usage>`,
|
|
136
128
|
`</task-notification>`,
|
|
@@ -573,7 +565,7 @@ export default function (pi) {
|
|
|
573
565
|
? `\n- Use \`schedule\` only when the user explicitly asked for scheduled / recurring / delayed execution (e.g. "every Monday", "in an hour"). Don't auto-schedule from vague intent like "monitor X" — run once now or ask.`
|
|
574
566
|
: "";
|
|
575
567
|
pi.registerTool(defineTool({
|
|
576
|
-
name:
|
|
568
|
+
name: SUBAGENT_TOOL_NAMES.AGENT,
|
|
577
569
|
label: "Agent",
|
|
578
570
|
description: `Launch a new agent to handle complex, multi-step tasks autonomously. Each agent type has specific capabilities and tools available to it.
|
|
579
571
|
|
|
@@ -673,7 +665,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
673
665
|
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
674
666
|
return new Text(text, 0, 0);
|
|
675
667
|
}
|
|
676
|
-
// Helper: build "haiku · thinking: high ·
|
|
668
|
+
// Helper: build "haiku · thinking: high · ↻5≤30 · 3 tool uses · 33.8k tokens" stats string
|
|
677
669
|
const stats = (d) => {
|
|
678
670
|
const parts = [];
|
|
679
671
|
if (d.modelName)
|
|
@@ -1042,7 +1034,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1042
1034
|
}));
|
|
1043
1035
|
// ---- get_subagent_result tool ----
|
|
1044
1036
|
pi.registerTool(defineTool({
|
|
1045
|
-
name:
|
|
1037
|
+
name: SUBAGENT_TOOL_NAMES.GET_RESULT,
|
|
1046
1038
|
label: "Get Agent Result",
|
|
1047
1039
|
description: "Check status and retrieve results from a background agent. Use the agent ID returned by Agent with run_in_background.",
|
|
1048
1040
|
promptSnippet: "Check status and retrieve results from a background agent",
|
|
@@ -1084,7 +1076,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1084
1076
|
statsParts.push(`Compactions: ${record.compactionCount}`);
|
|
1085
1077
|
statsParts.push(`Duration: ${duration}`);
|
|
1086
1078
|
let output = `Agent: ${record.id}\n` +
|
|
1087
|
-
`Type: ${displayName} | Status: ${record.status} | ${statsParts.join(" | ")}\n` +
|
|
1079
|
+
`Type: ${displayName} | Status: ${record.status}${getStatusNote(record.status)} | ${statsParts.join(" | ")}\n` +
|
|
1088
1080
|
`Description: ${record.description}\n\n`;
|
|
1089
1081
|
if (record.status === "running") {
|
|
1090
1082
|
output += "Agent is still running. Use wait: true or check back later.";
|
|
@@ -1112,7 +1104,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1112
1104
|
}));
|
|
1113
1105
|
// ---- steer_subagent tool ----
|
|
1114
1106
|
pi.registerTool(defineTool({
|
|
1115
|
-
name:
|
|
1107
|
+
name: SUBAGENT_TOOL_NAMES.STEER,
|
|
1116
1108
|
label: "Steer Agent",
|
|
1117
1109
|
description: "Send a steering message to a running agent. The message will interrupt the agent after its current tool execution " +
|
|
1118
1110
|
"and be injected into its conversation, allowing you to redirect its work mid-run. Only works on running agents.",
|
|
@@ -1322,7 +1314,11 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1322
1314
|
const session = record.session;
|
|
1323
1315
|
const activity = agentActivity.get(record.id);
|
|
1324
1316
|
await ctx.ui.custom((tui, theme, _keybindings, done) => {
|
|
1325
|
-
return new ConversationViewer(tui, session, record, activity, theme, done)
|
|
1317
|
+
return new ConversationViewer(tui, session, record, activity, theme, done, () => {
|
|
1318
|
+
if (manager.abort(record.id)) {
|
|
1319
|
+
ctx.ui.notify(`Stopped "${record.description}".`, "info");
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1326
1322
|
}, {
|
|
1327
1323
|
overlay: true,
|
|
1328
1324
|
overlayOptions: { anchor: "center", width: "90%", maxHeight: `${VIEWPORT_HEIGHT_PCT}%` },
|
package/dist/prompts.d.ts
CHANGED
|
@@ -16,12 +16,15 @@ export interface PromptExtras {
|
|
|
16
16
|
* Build the system prompt for an agent from its config.
|
|
17
17
|
*
|
|
18
18
|
* - "replace" mode: env header + config.systemPrompt (full control, no parent identity)
|
|
19
|
-
* - "append" mode:
|
|
19
|
+
* - "append" mode: parent system prompt + sub-agent context + env header + config.systemPrompt
|
|
20
20
|
* - "append" with empty systemPrompt: pure parent clone
|
|
21
21
|
*
|
|
22
|
-
* Both modes
|
|
22
|
+
* Both modes include an `<active_agent name="${config.name}"/>` tag so downstream
|
|
23
23
|
* extensions (e.g. permission/policy systems) can resolve per-agent policy
|
|
24
|
-
* inside the child session by parsing the system prompt.
|
|
24
|
+
* inside the child session by parsing the system prompt. In replace mode the tag
|
|
25
|
+
* is prepended; in append mode it follows the shared inherited content so the
|
|
26
|
+
* parent prompt forms an identical, cacheable byte prefix with the parent
|
|
27
|
+
* session (the LLM's KV cache can then reuse those tokens across every spawn).
|
|
25
28
|
*
|
|
26
29
|
* @param parentSystemPrompt The parent agent's effective system prompt (for append mode).
|
|
27
30
|
* @param extras Optional extra sections to inject (memory, preloaded skills).
|
package/dist/prompts.js
CHANGED
|
@@ -5,12 +5,15 @@
|
|
|
5
5
|
* Build the system prompt for an agent from its config.
|
|
6
6
|
*
|
|
7
7
|
* - "replace" mode: env header + config.systemPrompt (full control, no parent identity)
|
|
8
|
-
* - "append" mode:
|
|
8
|
+
* - "append" mode: parent system prompt + sub-agent context + env header + config.systemPrompt
|
|
9
9
|
* - "append" with empty systemPrompt: pure parent clone
|
|
10
10
|
*
|
|
11
|
-
* Both modes
|
|
11
|
+
* Both modes include an `<active_agent name="${config.name}"/>` tag so downstream
|
|
12
12
|
* extensions (e.g. permission/policy systems) can resolve per-agent policy
|
|
13
|
-
* inside the child session by parsing the system prompt.
|
|
13
|
+
* inside the child session by parsing the system prompt. In replace mode the tag
|
|
14
|
+
* is prepended; in append mode it follows the shared inherited content so the
|
|
15
|
+
* parent prompt forms an identical, cacheable byte prefix with the parent
|
|
16
|
+
* session (the LLM's KV cache can then reuse those tokens across every spawn).
|
|
14
17
|
*
|
|
15
18
|
* @param parentSystemPrompt The parent agent's effective system prompt (for append mode).
|
|
16
19
|
* @param extras Optional extra sections to inject (memory, preloaded skills).
|
|
@@ -49,7 +52,12 @@ You are operating as a sub-agent invoked to handle a specific task.
|
|
|
49
52
|
const customSection = config.systemPrompt?.trim()
|
|
50
53
|
? `\n\n<agent_instructions>\n${config.systemPrompt}\n</agent_instructions>`
|
|
51
54
|
: "";
|
|
52
|
-
|
|
55
|
+
// Place shared/stable content first so the LLM's KV cache can reuse the
|
|
56
|
+
// inherited prefix across all subagent invocations. The parent prompt is
|
|
57
|
+
// placed verbatim (no wrapper tag) so it forms an identical byte prefix
|
|
58
|
+
// with the parent session, maximising KV cache hits. The <active_agent>
|
|
59
|
+
// tag and env block vary per call and are placed after the cached prefix.
|
|
60
|
+
return identity + "\n\n" + bridge + "\n\n" + activeAgentTag + envBlock + customSection + extrasSuffix;
|
|
53
61
|
}
|
|
54
62
|
// "replace" mode — env header + the config's full system prompt
|
|
55
63
|
const replaceHeader = `You are a pi coding agent sub-agent.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* status-note.ts — Parenthetical status note appended to agent result text.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Explicit parenthetical note for a non-normal terminal outcome, so the parent
|
|
6
|
+
* agent can't mistake partial output for a completed result. Empty string for a
|
|
7
|
+
* clean completion (and any unknown/non-terminal status).
|
|
8
|
+
*
|
|
9
|
+
* `stopped` (a human aborted it) is deliberately distinct from `aborted` (the
|
|
10
|
+
* turn limit was hit) — the parent should treat human intervention differently
|
|
11
|
+
* from a budget cutoff.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getStatusNote(status: string): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* status-note.ts — Parenthetical status note appended to agent result text.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Explicit parenthetical note for a non-normal terminal outcome, so the parent
|
|
6
|
+
* agent can't mistake partial output for a completed result. Empty string for a
|
|
7
|
+
* clean completion (and any unknown/non-terminal status).
|
|
8
|
+
*
|
|
9
|
+
* `stopped` (a human aborted it) is deliberately distinct from `aborted` (the
|
|
10
|
+
* turn limit was hit) — the parent should treat human intervention differently
|
|
11
|
+
* from a budget cutoff.
|
|
12
|
+
*/
|
|
13
|
+
export function getStatusNote(status) {
|
|
14
|
+
switch (status) {
|
|
15
|
+
case "stopped":
|
|
16
|
+
return " (STOPPED BY THE USER before completion — output is partial; the task was NOT finished)";
|
|
17
|
+
case "aborted":
|
|
18
|
+
return " (aborted — hit the turn limit before completion; output may be incomplete)";
|
|
19
|
+
case "steered":
|
|
20
|
+
return " (wrapped up at the turn limit — output may be partial)";
|
|
21
|
+
default:
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -19,6 +19,9 @@ export interface AgentConfig {
|
|
|
19
19
|
displayName?: string;
|
|
20
20
|
description: string;
|
|
21
21
|
builtinToolNames?: string[];
|
|
22
|
+
/** Raw `ext:` selector entries from the `tools:` CSV, e.g. ["ext:foo", "ext:bar/x"].
|
|
23
|
+
* Presence of any entry flips extension tools to an explicit allowlist. */
|
|
24
|
+
extSelectors?: string[];
|
|
22
25
|
/** Tool denylist — these tools are removed even if `builtinToolNames` or extensions include them. */
|
|
23
26
|
disallowedTools?: string[];
|
|
24
27
|
/** true = inherit all, string[] = only listed, false = none */
|
|
@@ -66,15 +66,15 @@ export declare function formatTokens(count: number): string;
|
|
|
66
66
|
/**
|
|
67
67
|
* Token count with optional context-fill % and compaction-count annotations.
|
|
68
68
|
* Thresholds for percent: <70% dim, 70–85% warning, ≥85% error.
|
|
69
|
-
* Compaction count rendered as
|
|
69
|
+
* Compaction count rendered as `⇊N` in dim.
|
|
70
70
|
*
|
|
71
71
|
* "12.3k token" — no annotations
|
|
72
72
|
* "12.3k token (45%)" — percent only
|
|
73
|
-
* "12.3k token (
|
|
74
|
-
* "12.3k token (45% ·
|
|
73
|
+
* "12.3k token (⇊2)" — compactions only (e.g. right after compact)
|
|
74
|
+
* "12.3k token (45% · ⇊2)" — both
|
|
75
75
|
*/
|
|
76
76
|
export declare function formatSessionTokens(tokens: number, percent: number | null, theme: Theme, compactions?: number): string;
|
|
77
|
-
/** Format turn count with optional max limit: "
|
|
77
|
+
/** Format turn count with optional max limit: "↻5≤30" or "↻5". */
|
|
78
78
|
export declare function formatTurns(turnCount: number, maxTurns?: number | null): string;
|
|
79
79
|
/** Format milliseconds as human-readable duration. */
|
|
80
80
|
export declare function formatMs(ms: number): string;
|
package/dist/ui/agent-widget.js
CHANGED
|
@@ -36,12 +36,12 @@ export function formatTokens(count) {
|
|
|
36
36
|
/**
|
|
37
37
|
* Token count with optional context-fill % and compaction-count annotations.
|
|
38
38
|
* Thresholds for percent: <70% dim, 70–85% warning, ≥85% error.
|
|
39
|
-
* Compaction count rendered as
|
|
39
|
+
* Compaction count rendered as `⇊N` in dim.
|
|
40
40
|
*
|
|
41
41
|
* "12.3k token" — no annotations
|
|
42
42
|
* "12.3k token (45%)" — percent only
|
|
43
|
-
* "12.3k token (
|
|
44
|
-
* "12.3k token (45% ·
|
|
43
|
+
* "12.3k token (⇊2)" — compactions only (e.g. right after compact)
|
|
44
|
+
* "12.3k token (45% · ⇊2)" — both
|
|
45
45
|
*/
|
|
46
46
|
export function formatSessionTokens(tokens, percent, theme, compactions = 0) {
|
|
47
47
|
const tokenStr = formatTokens(tokens);
|
|
@@ -51,15 +51,15 @@ export function formatSessionTokens(tokens, percent, theme, compactions = 0) {
|
|
|
51
51
|
annot.push(theme.fg(color, `${Math.round(percent)}%`));
|
|
52
52
|
}
|
|
53
53
|
if (compactions > 0) {
|
|
54
|
-
annot.push(theme.fg("dim",
|
|
54
|
+
annot.push(theme.fg("dim", `⇊${compactions}`));
|
|
55
55
|
}
|
|
56
56
|
if (annot.length === 0)
|
|
57
57
|
return tokenStr;
|
|
58
58
|
return `${tokenStr} (${annot.join(" · ")})`;
|
|
59
59
|
}
|
|
60
|
-
/** Format turn count with optional max limit: "
|
|
60
|
+
/** Format turn count with optional max limit: "↻5≤30" or "↻5". */
|
|
61
61
|
export function formatTurns(turnCount, maxTurns) {
|
|
62
|
-
return maxTurns != null ?
|
|
62
|
+
return maxTurns != null ? `↻${turnCount}≤${maxTurns}` : `↻${turnCount}`;
|
|
63
63
|
}
|
|
64
64
|
/** Format milliseconds as human-readable duration. */
|
|
65
65
|
export function formatMs(ms) {
|
|
@@ -18,14 +18,22 @@ export declare class ConversationViewer implements Component {
|
|
|
18
18
|
private activity;
|
|
19
19
|
private theme;
|
|
20
20
|
private done;
|
|
21
|
+
/** Abort the agent shown here. Omitted → no stop affordance (e.g. read-only history). */
|
|
22
|
+
private onStop?;
|
|
21
23
|
private scrollOffset;
|
|
22
24
|
private autoScroll;
|
|
23
25
|
private unsubscribe;
|
|
24
26
|
private lastInnerW;
|
|
25
27
|
private closed;
|
|
26
|
-
|
|
28
|
+
/** Two-press confirm guard for the stop key, so a stray key can't kill the agent. */
|
|
29
|
+
private stopArmed;
|
|
30
|
+
constructor(tui: TUI, session: AgentSession, record: AgentRecord, activity: AgentActivity | undefined, theme: Theme, done: (result: undefined) => void,
|
|
31
|
+
/** Abort the agent shown here. Omitted → no stop affordance (e.g. read-only history). */
|
|
32
|
+
onStop?: (() => void) | undefined);
|
|
27
33
|
handleInput(data: string): void;
|
|
28
34
|
render(width: number): string[];
|
|
35
|
+
/** Stoppable only when a stop handler exists and the agent is still active. */
|
|
36
|
+
private isStoppable;
|
|
29
37
|
invalidate(): void;
|
|
30
38
|
dispose(): void;
|
|
31
39
|
private viewportHeight;
|
|
@@ -20,18 +20,24 @@ export class ConversationViewer {
|
|
|
20
20
|
activity;
|
|
21
21
|
theme;
|
|
22
22
|
done;
|
|
23
|
+
onStop;
|
|
23
24
|
scrollOffset = 0;
|
|
24
25
|
autoScroll = true;
|
|
25
26
|
unsubscribe;
|
|
26
27
|
lastInnerW = 0;
|
|
27
28
|
closed = false;
|
|
28
|
-
|
|
29
|
+
/** Two-press confirm guard for the stop key, so a stray key can't kill the agent. */
|
|
30
|
+
stopArmed = false;
|
|
31
|
+
constructor(tui, session, record, activity, theme, done,
|
|
32
|
+
/** Abort the agent shown here. Omitted → no stop affordance (e.g. read-only history). */
|
|
33
|
+
onStop) {
|
|
29
34
|
this.tui = tui;
|
|
30
35
|
this.session = session;
|
|
31
36
|
this.record = record;
|
|
32
37
|
this.activity = activity;
|
|
33
38
|
this.theme = theme;
|
|
34
39
|
this.done = done;
|
|
40
|
+
this.onStop = onStop;
|
|
35
41
|
this.unsubscribe = session.subscribe(() => {
|
|
36
42
|
if (this.closed)
|
|
37
43
|
return;
|
|
@@ -44,6 +50,23 @@ export class ConversationViewer {
|
|
|
44
50
|
this.done(undefined);
|
|
45
51
|
return;
|
|
46
52
|
}
|
|
53
|
+
// Stop/abort the agent (only while it can still be stopped). Two-press:
|
|
54
|
+
// first "x" arms, second confirms — any other key disarms.
|
|
55
|
+
if (matchesKey(data, "x")) {
|
|
56
|
+
if (this.isStoppable()) {
|
|
57
|
+
if (this.stopArmed) {
|
|
58
|
+
this.stopArmed = false;
|
|
59
|
+
this.onStop?.();
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
this.stopArmed = true;
|
|
63
|
+
}
|
|
64
|
+
this.tui.requestRender();
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (this.stopArmed)
|
|
69
|
+
this.stopArmed = false;
|
|
47
70
|
const totalLines = this.buildContentLines(this.lastInnerW).length;
|
|
48
71
|
const viewportHeight = this.viewportHeight();
|
|
49
72
|
const maxScroll = Math.max(0, totalLines - viewportHeight);
|
|
@@ -132,12 +155,22 @@ export class ConversationViewer {
|
|
|
132
155
|
? "100%"
|
|
133
156
|
: `${Math.round(((visibleStart + viewportHeight) / contentLines.length) * 100)}%`;
|
|
134
157
|
const footerLeft = th.fg("dim", `${contentLines.length} lines · ${scrollPct}`);
|
|
135
|
-
const
|
|
158
|
+
const scrollHint = th.fg("dim", "↑↓ scroll · PgUp/PgDn or Shift+↑↓ · Esc close");
|
|
159
|
+
// Stop hint goes first in the right group so it survives right-edge
|
|
160
|
+
// truncation on narrow terminals (the scroll hint is the expendable part).
|
|
161
|
+
const footerRight = this.isStoppable()
|
|
162
|
+
? (this.stopArmed ? th.fg("error", "x again to STOP") : th.fg("dim", "x stop")) +
|
|
163
|
+
th.fg("dim", " · ") + scrollHint
|
|
164
|
+
: scrollHint;
|
|
136
165
|
const footerGap = Math.max(1, innerW - visibleWidth(footerLeft) - visibleWidth(footerRight));
|
|
137
166
|
lines.push(row(footerLeft + " ".repeat(footerGap) + footerRight));
|
|
138
167
|
lines.push(hrBot);
|
|
139
168
|
return lines;
|
|
140
169
|
}
|
|
170
|
+
/** Stoppable only when a stop handler exists and the agent is still active. */
|
|
171
|
+
isStoppable() {
|
|
172
|
+
return !!this.onStop && (this.record.status === "running" || this.record.status === "queued");
|
|
173
|
+
}
|
|
141
174
|
invalidate() { }
|
|
142
175
|
dispose() {
|
|
143
176
|
this.closed = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tintinweb/pi-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "A pi extension extension that brings smart Claude Code-style autonomous sub-agents to pi.",
|
|
5
5
|
"author": "tintinweb",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"prepublishOnly": "npm run lint && npm run typecheck && npm run test && npm run build",
|
|
36
36
|
"test": "vitest run",
|
|
37
37
|
"test:watch": "vitest",
|
|
38
|
+
"test:e2e": "vitest run e2e --reporter=verbose",
|
|
38
39
|
"typecheck": "tsc --noEmit",
|
|
39
40
|
"lint": "biome check src/ test/",
|
|
40
41
|
"lint:fix": "biome check --fix src/ test/"
|