@aiwayds/dsh-tui-pi 0.1.1 → 0.4.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.
Files changed (62) hide show
  1. package/README.md +175 -41
  2. package/lib/agent-manager.d.ts +125 -0
  3. package/lib/agent-manager.js +298 -0
  4. package/lib/agent-manager.js.map +1 -0
  5. package/lib/agents.d.ts +30 -0
  6. package/lib/agents.js +489 -0
  7. package/lib/agents.js.map +1 -0
  8. package/lib/append-system.d.ts +2 -0
  9. package/lib/append-system.js +1 -1
  10. package/lib/append-system.js.map +1 -1
  11. package/lib/dsh-events.d.ts +12 -0
  12. package/lib/editor.d.ts +58 -0
  13. package/lib/editor.js +92 -0
  14. package/lib/editor.js.map +1 -1
  15. package/lib/footer.d.ts +46 -0
  16. package/lib/footer.js +65 -0
  17. package/lib/footer.js.map +1 -1
  18. package/lib/hotkeys.d.ts +93 -0
  19. package/lib/hotkeys.js +305 -0
  20. package/lib/hotkeys.js.map +1 -0
  21. package/lib/index.js +342 -61
  22. package/lib/index.js.map +1 -1
  23. package/lib/keymap.d.ts +243 -0
  24. package/lib/keymap.js +245 -0
  25. package/lib/keymap.js.map +1 -0
  26. package/lib/live-widgets.d.ts +59 -49
  27. package/lib/live-widgets.js +174 -117
  28. package/lib/live-widgets.js.map +1 -1
  29. package/lib/panels.d.ts +177 -0
  30. package/lib/panels.js +331 -0
  31. package/lib/panels.js.map +1 -0
  32. package/lib/selectors.d.ts +12 -1
  33. package/lib/selectors.js +1 -1
  34. package/lib/selectors.js.map +1 -1
  35. package/lib/session.d.ts +25 -0
  36. package/lib/session.js +173 -12
  37. package/lib/session.js.map +1 -1
  38. package/lib/settings.d.ts +1 -2
  39. package/lib/subagent-policy.d.ts +88 -0
  40. package/lib/subagent-policy.js +189 -0
  41. package/lib/subagent-policy.js.map +1 -0
  42. package/lib/subagent-viewer.d.ts +49 -0
  43. package/lib/subagent-viewer.js +523 -0
  44. package/lib/subagent-viewer.js.map +1 -0
  45. package/lib/theme/index.js +4 -0
  46. package/lib/theme/index.js.map +1 -1
  47. package/lib/theme/palette.d.ts +21 -3
  48. package/lib/theme/palette.js +27 -6
  49. package/lib/theme/palette.js.map +1 -1
  50. package/lib/theme-settings.d.ts +71 -9
  51. package/lib/theme-settings.js +145 -21
  52. package/lib/theme-settings.js.map +1 -1
  53. package/lib/tui.d.ts +23 -9
  54. package/lib/tui.js +110 -7
  55. package/lib/tui.js.map +1 -1
  56. package/package.json +3 -4
  57. package/patches/@earendil-works__pi-tui.patch +206 -2
  58. package/scripts/dev-install.mjs +87 -0
  59. package/scripts/link-dsh-closure.mjs +85 -0
  60. package/templates/agents/ArtyDuck.md +11 -0
  61. package/templates/agents/oldfox.md +33 -0
  62. package/templates/agents/workhorse.md +34 -0
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Subagent policy for the TUI.
3
+ *
4
+ * Three knobs, all read live from the `dsh-tui` settings section at every
5
+ * decision point — no watcher needed (the /settings browser hot-applies, so
6
+ * the next guard execution / turn count reads the new value):
7
+ * - `maxAgents` caps concurrent live children. A `tools.guard` registered on
8
+ * the plugin root ctx denies model-facing spawn tools once the bridge's
9
+ * live child count meets the cap. Registered globally, every agent in the
10
+ * process obeys — this TUI's session and any delegation nesting. The
11
+ * workflow/ralph fan-out bypasses the tool pipeline (its worker thread
12
+ * spawns through the subagent provider directly), so a `subagent/start`
13
+ * listener prunes any newcomer that slips past the guard.
14
+ * - `disableSubagent` disables the plain native `subagent` tool: its calls
15
+ * are denied for every agent (and it is hidden from the main agent's
16
+ * catalog), so delegation goes through registered agent definitions
17
+ * (`~/.dsh/agents/*.md` via the registry's `use_agent`). `subagent_fork`,
18
+ * `workflow` and `ralph` stay available.
19
+ * - `maxRounds` caps a child's completed turns: on the bridge's `onTurnCount`
20
+ * the policy injects one plugin-sourced user message telling the child to
21
+ * wrap up. Queued as the child's next turn, it never interrupts work
22
+ * already underway.
23
+ */
24
+ import { createUserMessage } from '@deepseek-ai/dsh-llm';
25
+ import { SessionId } from '@deepseek-ai/dsh-session';
26
+ import { readSubagentLimits } from "./theme-settings.js";
27
+ /**
28
+ * Tool names the maxAgents guard intercepts: the native subagent tool, the
29
+ * one-shot fork variant, the workflow fan-out entrypoint, its ralph-loop
30
+ * sibling, and registry plugins' `use_agent`.
31
+ */
32
+ export const SPAWN_TOOLS = [
33
+ 'subagent',
34
+ 'subagent_fork',
35
+ 'workflow',
36
+ 'ralph',
37
+ 'use_agent',
38
+ ];
39
+ /**
40
+ * The native plain one-shot spawn tool - the only spawn tool `disableSubagent`
41
+ * fences. Deliberately a single name: the TUI's user delegates through
42
+ * registered agents (`~/.dsh/agents/*.md` via `use_agent`) and wants ONLY the
43
+ * plain `subagent` tool off; `subagent_fork` (fork a running session),
44
+ * `workflow` and `ralph` (fan-out loops) stay available.
45
+ */
46
+ export const NATIVE_SPAWN_TOOLS = [
47
+ 'subagent',
48
+ ];
49
+ /**
50
+ * Hide the plain `subagent` tool from ONE agent's tool catalog (best-effort).
51
+ * The disableSubagent guard DENIES calls at execution; this additionally
52
+ * makes the tool INVISIBLE to the model, so the agent sees the registered
53
+ * `use_agent` as its delegation entry and never attempts the fenced tool.
54
+ *
55
+ * `tools.restrict` requires a scoped context (it throws on a plain plugin
56
+ * ctx) - the agent setup passes its scoped `agentCtx`, exactly the surface
57
+ * dsh-subagent uses for `deep: 0` leaves. Best-effort by design: an absent
58
+ * tools service, or a restrict failure (an unknown tool name in this
59
+ * profile, a transient registration race), degrades silently - the live
60
+ * disableSubagent guard remains the enforcement backstop, so the fence is
61
+ * NEVER weaker for hiding. Restriction is a creation-time snapshot: a
62
+ * disableSubagent toggle takes effect for agents created afterwards (the
63
+ * guard, being live, applies immediately in both directions).
64
+ */
65
+ export function installSpawnToolFence(agentCtx) {
66
+ const tools = agentCtx.get('tools');
67
+ if (tools?.restrict === undefined)
68
+ return;
69
+ try {
70
+ tools.restrict({ deny: [...NATIVE_SPAWN_TOOLS] });
71
+ }
72
+ catch {
73
+ // Cosmetic hide only - never fail the agent setup over it. The guard
74
+ // below still denies the subagent tool at execution time.
75
+ }
76
+ }
77
+ /** Injected into a child that reached `maxRounds` — wrap up and report back. */
78
+ export const SUMMARY_MESSAGE = '总结和结束这个任务,汇报情况。';
79
+ /**
80
+ * Install the subagent policy on the plugin root ctx.
81
+ *
82
+ * @param ctx - plugin context. The settings-service lookup inside the helpers
83
+ * is defensive: a settings-less deployment resolves the defaults, so the
84
+ * policy still enforces its documented caps.
85
+ * @param state - the host's live view (bridge.getLiveChildren /
86
+ * bridge.getTurnCount). The guard and the round injection read it at every
87
+ * decision — no snapshot, no watch.
88
+ * @returns the policy handle wired to the bridge's `onTurnCount`.
89
+ */
90
+ export function applySubagentPolicy(ctx, state) {
91
+ const disposers = [];
92
+ const injected = new Set();
93
+ // maxAgents guard: global (plugin root ctx), every spawn-tool call is
94
+ // denied while live children are at the cap. Zero disables the guard.
95
+ // The cap is approximate, not a hard admission lock: a burst of parallel
96
+ // spawn calls can briefly overshoot before the bridge's firehose discovery
97
+ // counts the newcomers — the subagent/start backstop below prunes the
98
+ // overshoot, and the next spawn's guard reads the caught-up count.
99
+ //
100
+ // disableSubagent fence: the plain native `subagent` tool is denied
101
+ // outright (checked before the cap - a tool violation is the reason even
102
+ // when the cap would also deny). `use_agent`, the fork/workflow/ralph
103
+ // variants and every non-spawn tool pass through to the cap check below.
104
+ const tools = ctx.get('tools');
105
+ if (tools?.guard !== undefined) {
106
+ disposers.push(tools.guard((exec) => {
107
+ if (!SPAWN_TOOLS.includes(exec.name))
108
+ return undefined;
109
+ if (readSubagentLimits(ctx).disableSubagent && NATIVE_SPAWN_TOOLS.includes(exec.name)) {
110
+ return `Tool "subagent" is disabled here - delegation goes through registered agents. `
111
+ + 'Dispatch the work through the use_agent tool with one of the registered agent names instead.';
112
+ }
113
+ const maxAgents = readSubagentLimits(ctx).maxAgents;
114
+ if (maxAgents <= 0)
115
+ return undefined;
116
+ const live = state.getLive();
117
+ if (live.length < maxAgents)
118
+ return undefined;
119
+ const running = live.map((agent) => agent.label).join(', ');
120
+ return `Agent limit reached (${live.length}/${maxAgents}): ${running} still running — wait for one to finish or use list_agents before spawning more.`;
121
+ }));
122
+ }
123
+ // Workflow/ralph backstop: the worker thread fans children out through the
124
+ // subagent provider, never through the tool pipeline — the only leak past
125
+ // the guard. Prune a newcomer whose start overshoots the cap. The event is
126
+ // foreign to this bundle's type environment (`@deepseek-ai/dsh-subagent` is
127
+ // not installed), so the subscription rides the base event bus rather than
128
+ // the typed `ctx.on`.
129
+ const eventDisposer = ctx.events.on('subagent/start', (info) => {
130
+ if (info?.id === undefined)
131
+ return;
132
+ const maxAgents = readSubagentLimits(ctx).maxAgents;
133
+ if (maxAgents <= 0)
134
+ return;
135
+ // Exclude the newcomer itself: whether or not its own session events
136
+ // have reached the bridge's count yet (the subagent/start notification
137
+ // and the child's first firehose event race), the child is pruned only
138
+ // when the OTHER live children already fill the cap — a legitimate
139
+ // Nth-at-cap child is never cancelled on either ordering.
140
+ const live = state.getLive().filter(view => view.childId !== String(info.id));
141
+ if (live.length < maxAgents)
142
+ return;
143
+ ctx.agents.get(info.id)?.cancel({
144
+ kind: 'hook',
145
+ reason: 'over the dsh-tui maxAgents policy cap — prune a fan-out child',
146
+ });
147
+ });
148
+ disposers.push(eventDisposer);
149
+ /**
150
+ * Bridge turn-count sink: inject the summary message exactly once per
151
+ * child, at the first turn that reaches a positive `maxRounds`. A missing
152
+ * agent (cold, or transiently unregistered) is skipped silently; a settled
153
+ * child is skipped too — injecting into a finished one would wake it for a
154
+ * pointless wrap-up round (continuable children that resume later get their
155
+ * chance on the next counted turn). Repeated injections are impossible by
156
+ * construction (the set is only written on success).
157
+ */
158
+ function onTurnCount(childId, count) {
159
+ if (injected.has(childId))
160
+ return;
161
+ const maxRounds = readSubagentLimits(ctx).maxRounds;
162
+ if (maxRounds <= 0)
163
+ return;
164
+ if (count < maxRounds)
165
+ return;
166
+ if (state.isSettled(childId))
167
+ return;
168
+ const agent = ctx.agents.get(SessionId(childId));
169
+ if (agent === undefined)
170
+ return;
171
+ injected.add(childId);
172
+ agent.followup(createUserMessage({
173
+ content: [{ type: 'text', text: SUMMARY_MESSAGE }],
174
+ source: { kind: 'plugin', plugin: 'dsh-tui-pi' },
175
+ }));
176
+ }
177
+ return {
178
+ onTurnCount,
179
+ dispose() {
180
+ for (const dispose of disposers.splice(0)) {
181
+ try {
182
+ dispose();
183
+ }
184
+ catch { /* contained */ }
185
+ }
186
+ },
187
+ };
188
+ }
189
+ //# sourceMappingURL=subagent-policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subagent-policy.js","sourceRoot":"","sources":["../src/subagent-policy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The subagent viewer — the Ctrl+G surface. Two levels, both on the shared
3
+ * PanelHost overlay (80%): a SelectList picker over the bridge's child views,
4
+ * then a live transcript panel for one child's buffered event log.
5
+ *
6
+ * Picker rows (running first, then the five most recent settled) carry the
7
+ * full picture of a delegation: status icon, label, delegation mode, rounds
8
+ * against the policy cap (`maxRounds` from `readSubagentLimits`, only shown
9
+ * when > 0), token spend (k) and elapsed time. Settled rows read as faded
10
+ * because their status icon and the muted description column mark the outcome.
11
+ *
12
+ * Transcript panel: one human-readable line per buffered SessionEvent
13
+ * (user/assistant messages, tool calls + truncated results, turns, descriptor,
14
+ * todos). The log is a capped per-child ring buffer (2000 events); a dropped
15
+ * head is reported at the top. While the panel is open a ~300ms timer rebuilds
16
+ * the lines from the bridge and re-renders, and the view follows the tail like
17
+ * a ScrollView follow:'end' (an explicit scroll-up detaches; reaching the
18
+ * bottom re-attaches). Esc closes, or a deliberate double-`x` within
19
+ * `DOUBLE_PRESS_MS`.
20
+ */
21
+ import type { Context } from '@deepseek-ai/cordis';
22
+ import { type SelectItem, type TUI } from '@earendil-works/pi-tui';
23
+ import type { AgentView } from './dsh-events.ts';
24
+ import type { DshSessionBridge } from './session.ts';
25
+ import { type TuiTheme } from './theme/index.ts';
26
+ /**
27
+ * The picker rows: running children first (the live items the key is for),
28
+ * then the five most recent settled ones (newest settle first). Each row
29
+ * reads `⠋ label [mode] · rounds N[/max]` with the token spend and elapsed
30
+ * seconds in the muted description column.
31
+ */
32
+ export declare function pickerItems(views: readonly AgentView[], getTurnCount: (childId: string) => number, maxRounds: number): SelectItem[];
33
+ /**
34
+ * The next selected index after a picker items swap, preserving the selection
35
+ * by item value. `previous === undefined` (no selection yet) starts at 0; a
36
+ * value that dropped off the new list (e.g. a settled child fell out of the
37
+ * recent-settled cap) clamps to the last row rather than resetting the user
38
+ * to the top. Pure and exported for the regression test.
39
+ */
40
+ export declare function nextSelectedIndex(items: readonly SelectItem[], previous: string | undefined): number;
41
+ /**
42
+ * Open the subagent picker / viewer. A SelectList of the bridge's child views
43
+ * (running first, then the five recent settled) opens; Enter opens a
44
+ * live-refreshing transcript panel for that child. An empty list (no running,
45
+ * no settled) closes immediately. Focus returns to `restoreFocus` on close;
46
+ * the promise settles when the viewer closes (value is meaningless — index.ts
47
+ * never branches on it).
48
+ */
49
+ export declare function openSubagentViewer(ctx: Context, tui: TUI, theme: TuiTheme, bridge: DshSessionBridge, restoreFocus: () => void): Promise<void>;