@mjasnikovs/pi-task 0.18.47 → 0.18.49
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 +10 -10
- package/dist/config/register.d.ts +62 -1
- package/dist/config/register.js +150 -87
- package/dist/shared/pkg-version.d.ts +8 -0
- package/dist/shared/pkg-version.js +20 -0
- package/dist/workers/html-clean.js +2 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://www.npmjs.com/package/@mjasnikovs/pi-task)
|
|
10
10
|
[](./LICENSE)
|
|
11
11
|
[](https://www.npmjs.com/package/@earendil-works/pi-coding-agent)
|
|
12
|
-
[](#development)
|
|
13
13
|
[](./tsconfig.json)
|
|
14
14
|
|
|
15
15
|
</div>
|
|
@@ -65,7 +65,7 @@ A whole plan — `/task-auto` splits it into an ordered task list and runs each
|
|
|
65
65
|
| `/task-auto <feature>` | Plan a feature into a task list and run each title through `/task` in order (resumable). |
|
|
66
66
|
| `/task-auto-resume [--unattended]` | Resume the active `/task-auto` run at the next unfinished task. `--unattended` is the boot-hook form: in-flight runs only. |
|
|
67
67
|
| `/task-auto-cancel` | Stop the `/task-auto` loop after the current task (still resumable). |
|
|
68
|
-
| `/task-config` | Toggle pi-task settings in an editor dialog: remote
|
|
68
|
+
| `/task-config` | Toggle pi-task settings in an editor dialog: remote control, compress thinking, auto-commit, verify work, enforce guidelines, project tour, command timeout, stuck reply retry, and one `ext:` toggle per installed host extension. |
|
|
69
69
|
| `/remote` | Show the QR code & URLs for the web view (`/remote stop` to stop). Answer grill questions, start tasks, and watch progress from your phone. |
|
|
70
70
|
|
|
71
71
|
## The pipeline
|
|
@@ -171,25 +171,25 @@ Run `/task-config` to toggle pi-task's behavior in an editor dialog. Settings pe
|
|
|
171
171
|
|
|
172
172
|
| Setting | Default | What it does |
|
|
173
173
|
| --- | --- | --- |
|
|
174
|
-
| **remote** | on | The remote UI server (QR code, phone access). Turn off to never start it. |
|
|
175
|
-
| **compress
|
|
174
|
+
| **remote control** | on | The remote UI server (QR code, phone access). Turn off to never start it. |
|
|
175
|
+
| **compress thinking** | on | After each message, compresses the model's `<think>` blocks down to the decisions/constraints/facts that matter later — keeping long local-model runs from drowning their own context in self-talk. |
|
|
176
176
|
| **auto-commit** | on | Snapshots the working tree into one git commit per `/task-auto` sub-task (see above). |
|
|
177
|
-
| **orientation** | on | Pre-reads the project's core files (manifest, config, domain types, schema, entrypoints, API surface) once and hands the contents to the read-heavy research workers, so they skip re-discovering the same files cold. Bounded by a hard byte budget; applied only where it helps (FILES/APIS workers). |
|
|
178
177
|
| **verify work** | on | After each `/task` (and `/task-auto` task) implements — but **before** it's checked off or committed — actually **runs** the spec's own `VERIFY` block in the real workspace. pi-task otherwise only _authors_ a VERIFY block and never executes it, so a task that doesn't build is indistinguishable from one that works. A fresh `read` + `bash` child of the same local model runs the declared check, observes the real output, and reports **PASS/FAIL** (a legitimately no-op VERIFY is a PASS). On FAIL the run doesn't dead-stop: you get a boxed picker — **Autofix** (re-run the implementation turn against the failure, then re-verify; no attempt cap) or **Accept** (override a misjudged artifact) — and dismissing it pauses the run, resumable. A genuine clean pass is also the behavioral signal that lets **enforce guidelines** fix in place (see below). |
|
|
179
178
|
| **enforce guidelines** | on | After each `/task` (and `/task-auto` task) is committed, re-checks that commit's work against the project's `AGENTS.md` / `CLAUDE.md` (in the working directory). A bare fix-in-place pass trashes working code (A/B-proven), so enforcement is gated on the **verify work** signal. **With** a genuine verify pass: a fresh `read` + `edit` child of the same local model reads the **last commit's** diff and fixes violations in place; its fixes are committed **separately** as an `ENFORCE GUIDELINES` commit, then the verify signal is re-run against the enforced tree — a regression **reverts** the enforce commit and keeps the verified work. **Without** that signal (verify off, no spec, or an accept-override): the pass runs read-only and only **reports** violations, never rewrites logic. Either way a violation it can't clear (or a pass that can't run) only **warns** — the task commit already landed, so the run continues. Skipped when nothing was committed for the task. |
|
|
179
|
+
| **project tour** | on | Pre-reads the project's core files (manifest, config, domain types, schema, entrypoints, API surface) once and hands the contents to the read-heavy research workers, so they skip re-discovering the same files cold. Bounded by a hard byte budget; applied only where it helps (FILES/APIS workers). |
|
|
180
180
|
| **parallel research** | off | Run the four research workers concurrently instead of one at a time. Leave off on a single-GPU local backend (concurrent streams split the GPU and slow each other down); turn on only for a parallel-capable model server. |
|
|
181
181
|
| **research cache** | on | Cache docs/search/fetch worker results for the duration of one `/task-auto` run so sibling tasks re-asking the same package/URL + query reuse the first pipeline's digest instead of re-fetching. Per-run isolated, external-only (project-source `.` lookups excluded), success-only. |
|
|
182
|
-
| **search
|
|
182
|
+
| **search engine** | Exa | Engine behind `pi-worker-search` and freshness/enrichment checks. **Exa** (default) and **DuckDuckGo** need no API key; **Brave** requires `BRAVE_SEARCH_API_KEY`. |
|
|
183
183
|
| **command timeout** | 15 min | Wall-clock ceiling on a **single** tool execution. Local models routinely run a command that never returns (a hung build, a dev server, a check with no timeout) and the run wedges until you abort by hand — pi's bash tool has an optional timeout with no default, so this is the missing one. One knob, two surfaces: in the main session the overrun call is cancelled (killing the tool's whole process tree) plus a reminder turn; in the verify/fix gate children the child is killed and re-spawned with a hint, halving the ceiling on repeat hangs. Choices: 5/10/15/30 min or **off** — off unguards both surfaces, gates included. |
|
|
184
|
-
| **
|
|
184
|
+
| **stuck reply retry** | 10 min | Inactivity ceiling on the **model stream**. A hung or silently-dropped stream throws nothing at all, so neither the connection-error retry (it needs a reported error) nor the **command timeout** (tool calls only) nor the dead-backend stall guard (a reachable endpoint reads as proof of life) can see it — an mx5 run lost ~2.9h to three of them while the model server stayed healthy. Measured as time since the **last stream event of any kind**, so a slow model emitting one token every 30s is never touched, and it pauses while a tool runs. On expiry the main session aborts the turn (through the same channel the command watchdog uses) and posts a resume reminder; a child is killed and routed into the existing connection-error retry. Choices: 5/10/20/30 min or **off**. Keep it generous on local backends — prompt processing on a large context legitimately emits nothing for minutes. |
|
|
185
185
|
| **yolo mode** | off | **Unattended runs.** Wherever pi-task would stop and ask, it takes the option already marked RECOMMENDED, stamps the artifact `(YOLO)` so an audit can tell a machine decided, and shows no prompt at all — clarify/grill answers, the verify-FAIL picker (auto-**Accept**, recorded as a yolo debt), and the final-gate picker (autofix while the budget lasts, then leave the run FAILED). A question with no recommendation is **skipped**, never invented. For throwaway/test projects nobody is watching; a real run should decide these itself. |
|
|
186
|
-
| **
|
|
186
|
+
| **ext: …** | all off | One toggle per installed host `pi` extension, loading it into every child session by explicit path. Children otherwise run with extensions off, so a provider registered by an extension (e.g. `pi-lmstudio`) doesn't exist in them and they can't resolve the default model. Children also inherit the extension's tools and hooks, so only enable ones you trust. The list is strictly additive (discovery stays off), and an entry whose file is gone is skipped at spawn time, never fatal. |
|
|
187
187
|
|
|
188
188
|
## Configuration
|
|
189
189
|
|
|
190
190
|
| Variable | Used by | Notes |
|
|
191
191
|
| --- | --- | --- |
|
|
192
|
-
| `BRAVE_SEARCH_API_KEY` / `BRAVE_API_KEY` | `pi-worker-search`, research enrichment | Required only when the **Brave** search
|
|
192
|
+
| `BRAVE_SEARCH_API_KEY` / `BRAVE_API_KEY` | `pi-worker-search`, research enrichment | Required only when the **Brave** search engine is selected in `/task-config`. |
|
|
193
193
|
| `XDG_CACHE_HOME` | `pi-worker-docs` | Overrides the docs cache location (defaults to `~/.cache`). |
|
|
194
194
|
| `XDG_DATA_HOME` | remote push | Where the VAPID keypair is stored (defaults to `~/.local/share`). |
|
|
195
195
|
| `PI_REMOTE_PUSH_SUBJECT` | remote push | VAPID JWT `sub` contact. Defaults to the project URL; set your own `mailto:you@domain.com` or `https://…`. |
|
|
@@ -202,7 +202,7 @@ Tasks are persisted to `<cwd>/.pi-tasks/TASK_NNNN.md`. Add `.pi-tasks/` to your
|
|
|
202
202
|
|
|
203
203
|
```sh
|
|
204
204
|
bun install
|
|
205
|
-
bun run test #
|
|
205
|
+
bun run test # 2078 tests across 129 files
|
|
206
206
|
bun run lint # prettier + eslint + tsc --noEmit
|
|
207
207
|
bun run build # tsc → dist/
|
|
208
208
|
```
|
|
@@ -1,5 +1,42 @@
|
|
|
1
|
-
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import type { Component } from '@earendil-works/pi-tui';
|
|
3
|
+
import { type PiTaskConfig } from './config.js';
|
|
2
4
|
import { type InstalledExtension } from './extension-list.js';
|
|
5
|
+
type Theme = ExtensionCommandContext['ui']['theme'];
|
|
6
|
+
/**
|
|
7
|
+
* Frames a child component (the settings list) in a rounded border with a title
|
|
8
|
+
* woven into the top edge. Without this the overlay's content sits flush against
|
|
9
|
+
* the chat scrollback and reads as just more console text; the border gives it a
|
|
10
|
+
* distinct panel. Layout mirrors {@link renderQuestionBox}'s box chrome: render
|
|
11
|
+
* the child at the inner width, pad each line so the right edge lines up, then
|
|
12
|
+
* frame. Input and invalidation are forwarded straight through to the child.
|
|
13
|
+
*/
|
|
14
|
+
declare class BorderedBox implements Component {
|
|
15
|
+
private readonly child;
|
|
16
|
+
private readonly title;
|
|
17
|
+
private readonly border;
|
|
18
|
+
private readonly titleColor;
|
|
19
|
+
/**
|
|
20
|
+
* Body lines to pad out to. The settings list grows and shrinks with the
|
|
21
|
+
* selected item's description, so without a floor the whole panel jumps
|
|
22
|
+
* a few rows every time the cursor moves. Padding to the tallest layout
|
|
23
|
+
* keeps the border still while the contents change.
|
|
24
|
+
*/
|
|
25
|
+
private readonly minBodyLines;
|
|
26
|
+
constructor(child: Component & {
|
|
27
|
+
handleInput(data: string): void;
|
|
28
|
+
}, title: string, border: (s: string) => string, titleColor: (s: string) => string,
|
|
29
|
+
/**
|
|
30
|
+
* Body lines to pad out to. The settings list grows and shrinks with the
|
|
31
|
+
* selected item's description, so without a floor the whole panel jumps
|
|
32
|
+
* a few rows every time the cursor moves. Padding to the tallest layout
|
|
33
|
+
* keeps the border still while the contents change.
|
|
34
|
+
*/
|
|
35
|
+
minBodyLines?: number);
|
|
36
|
+
render(width: number): string[];
|
|
37
|
+
invalidate(): void;
|
|
38
|
+
handleInput(data: string): void;
|
|
39
|
+
}
|
|
3
40
|
export declare function extensionItems(extensions: InstalledExtension[], whitelist: readonly string[]): {
|
|
4
41
|
id: string;
|
|
5
42
|
label: string;
|
|
@@ -9,4 +46,28 @@ export declare function extensionItems(extensions: InstalledExtension[], whiteli
|
|
|
9
46
|
}[];
|
|
10
47
|
/** Apply an extension toggle to the config's whitelist (idempotent both ways). */
|
|
11
48
|
export declare function applyExtensionToggle(whitelist: readonly string[], entryPath: string, on: boolean): string[];
|
|
49
|
+
/**
|
|
50
|
+
* Tallest body the settings list can render, so {@link BorderedBox} can pad
|
|
51
|
+
* every frame to it and hold the border still. Mirrors SettingsList's own
|
|
52
|
+
* layout: one pad row, the visible rows, the scroll counter, a blank, the
|
|
53
|
+
* selected item's wrapped description, a blank, the key hint.
|
|
54
|
+
*/
|
|
55
|
+
export declare function settingsBodyHeight(descriptions: string[], maxVisible: number, wrapWidth: number): number;
|
|
56
|
+
/** A settings row as {@link SettingsList} wants it. */
|
|
57
|
+
export type PanelItem = {
|
|
58
|
+
id: string;
|
|
59
|
+
label: string;
|
|
60
|
+
description: string;
|
|
61
|
+
currentValue: string;
|
|
62
|
+
values: string[];
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Builds the framed settings panel. Split out of the command handler so the
|
|
66
|
+
* exact component the overlay shows can be rendered to a string in a test or a
|
|
67
|
+
* preview script, rather than only being inspectable by opening the TUI.
|
|
68
|
+
*/
|
|
69
|
+
export declare function createSettingsPanel(items: PanelItem[], theme: Theme, onChange: (id: string, newValue: string) => void, onCancel: () => void): BorderedBox;
|
|
70
|
+
/** The full settings row list for the current config, in menu order. */
|
|
71
|
+
export declare function panelItems(cfg: PiTaskConfig, installed: InstalledExtension[]): PanelItem[];
|
|
12
72
|
export declare function registerConfig(pi: ExtensionAPI): void;
|
|
73
|
+
export {};
|
package/dist/config/register.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { SettingsList, visibleWidth } from '@earendil-works/pi-tui';
|
|
1
|
+
import { SettingsList, visibleWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
|
|
2
2
|
import { registerBridgeCommand } from '../remote/bridge.js';
|
|
3
|
+
import { readPkgVersion } from '../shared/pkg-version.js';
|
|
3
4
|
import { SEARCH_PROVIDERS, SEARCH_PROVIDER_LABELS, providerForLabel } from '../workers/search-types.js';
|
|
4
5
|
import { COMMAND_TIMEOUT_OPTIONS, getConfig, saveConfig, STREAM_INACTIVITY_OPTIONS } from './config.js';
|
|
5
6
|
import { listInstalledExtensions } from './extension-list.js';
|
|
6
|
-
|
|
7
|
+
// Version in the title so a bug report or screenshot says which build it came
|
|
8
|
+
// from without anyone having to go look it up.
|
|
9
|
+
const CONFIG_TITLE = `pi-task ${readPkgVersion()} settings`;
|
|
7
10
|
/**
|
|
8
11
|
* Frames a child component (the settings list) in a rounded border with a title
|
|
9
12
|
* woven into the top edge. Without this the overlay's content sits flush against
|
|
@@ -17,11 +20,20 @@ class BorderedBox {
|
|
|
17
20
|
title;
|
|
18
21
|
border;
|
|
19
22
|
titleColor;
|
|
20
|
-
|
|
23
|
+
minBodyLines;
|
|
24
|
+
constructor(child, title, border, titleColor,
|
|
25
|
+
/**
|
|
26
|
+
* Body lines to pad out to. The settings list grows and shrinks with the
|
|
27
|
+
* selected item's description, so without a floor the whole panel jumps
|
|
28
|
+
* a few rows every time the cursor moves. Padding to the tallest layout
|
|
29
|
+
* keeps the border still while the contents change.
|
|
30
|
+
*/
|
|
31
|
+
minBodyLines = 0) {
|
|
21
32
|
this.child = child;
|
|
22
33
|
this.title = title;
|
|
23
34
|
this.border = border;
|
|
24
35
|
this.titleColor = titleColor;
|
|
36
|
+
this.minBodyLines = minBodyLines;
|
|
25
37
|
}
|
|
26
38
|
render(width) {
|
|
27
39
|
const innerWidth = Math.max(1, width - 4); // "│ " + content + " │"
|
|
@@ -33,7 +45,13 @@ class BorderedBox {
|
|
|
33
45
|
const top = rest >= 0 ?
|
|
34
46
|
this.border(`╭${dash(lead)}`) + this.titleColor(tag) + this.border(`${dash(rest)}╮`)
|
|
35
47
|
: this.border(`╭${dash(width - 2)}╮`);
|
|
36
|
-
|
|
48
|
+
// One blank row under the title so the first setting is not welded to
|
|
49
|
+
// the border, then the child, then padding up to the stable height.
|
|
50
|
+
const childLines = this.child.render(innerWidth);
|
|
51
|
+
const raw = ['', ...childLines];
|
|
52
|
+
while (raw.length < this.minBodyLines)
|
|
53
|
+
raw.push('');
|
|
54
|
+
const body = raw.map(line => {
|
|
37
55
|
const pad = innerWidth - visibleWidth(line);
|
|
38
56
|
const padded = pad > 0 ? line + ' '.repeat(pad) : line;
|
|
39
57
|
return this.border('│ ') + padded + this.border(' │');
|
|
@@ -53,83 +71,95 @@ class BorderedBox {
|
|
|
53
71
|
* toggle on/off; enum settings list their values and cycle through them.
|
|
54
72
|
*/
|
|
55
73
|
const ITEMS = [
|
|
56
|
-
{
|
|
74
|
+
{
|
|
75
|
+
id: 'remote',
|
|
76
|
+
label: 'remote control',
|
|
77
|
+
description: 'Serve the task UI on your local network so you can follow and steer a run from '
|
|
78
|
+
+ 'your phone. Prints a QR code to scan when it starts'
|
|
79
|
+
},
|
|
57
80
|
{
|
|
58
81
|
id: 'compressReasoning',
|
|
59
|
-
label: 'compress
|
|
60
|
-
description: '
|
|
82
|
+
label: 'compress thinking',
|
|
83
|
+
description: "Shrink the model's thinking blocks once it has moved on, so a long run keeps more "
|
|
84
|
+
+ 'room for the work itself'
|
|
61
85
|
},
|
|
62
86
|
{
|
|
63
87
|
id: 'autoCommit',
|
|
64
88
|
label: 'auto-commit',
|
|
65
|
-
description: 'git commit
|
|
66
|
-
|
|
67
|
-
{
|
|
68
|
-
id: 'orientation',
|
|
69
|
-
label: 'orientation',
|
|
70
|
-
description: 'Pre-supply the project core (manifest, types, schema…) to the read-heavy research workers'
|
|
89
|
+
description: 'Make a git commit before and after every sub-task, so each step is a checkpoint '
|
|
90
|
+
+ 'you can read back or roll back to'
|
|
71
91
|
},
|
|
72
92
|
{
|
|
73
93
|
id: 'verifyWork',
|
|
74
94
|
label: 'verify work',
|
|
75
|
-
description:
|
|
95
|
+
description: 'When a task says it is done, actually run the checks its spec asks for and report '
|
|
96
|
+
+ "PASS or FAIL instead of taking the model's word for it. This is also what lets "
|
|
97
|
+
+ '"enforce guidelines" fix things safely. /task waits for the work to finish'
|
|
76
98
|
},
|
|
77
99
|
{
|
|
78
100
|
id: 'enforceGuidelines',
|
|
79
101
|
label: 'enforce guidelines',
|
|
80
|
-
description:
|
|
102
|
+
description: 'Check what each task committed against your AGENTS.md / CLAUDE.md rules. With '
|
|
103
|
+
+ '"verify work" on it also fixes what it finds, undoing any fix that breaks the '
|
|
104
|
+
+ 'checks; on its own it only reports. /task waits for the work to finish'
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: 'orientation',
|
|
108
|
+
label: 'project tour',
|
|
109
|
+
description: 'Show the research workers the shape of the project first — package manifest, '
|
|
110
|
+
+ 'types, schema — so they spend their steps on the question instead of on finding '
|
|
111
|
+
+ 'their way around'
|
|
81
112
|
},
|
|
82
113
|
{
|
|
83
114
|
id: 'parallelResearchWorkers',
|
|
84
115
|
label: 'parallel research',
|
|
85
|
-
description: 'Run the 4 research workers
|
|
116
|
+
description: 'Run the 4 research workers at once instead of one after another. Only faster if '
|
|
117
|
+
+ 'your model backend can answer several requests at the same time — on a single '
|
|
118
|
+
+ 'local GPU it is measurably slower, so leave it off there'
|
|
86
119
|
},
|
|
87
120
|
{
|
|
88
121
|
id: 'researchCache',
|
|
89
122
|
label: 'research cache',
|
|
90
|
-
description: '
|
|
123
|
+
description: 'Remember docs and web pages for the length of one run, so later tasks reuse what '
|
|
124
|
+
+ 'the first one already fetched instead of downloading it again. Only external '
|
|
125
|
+
+ 'sources, only successful fetches, and it is dropped when the run ends'
|
|
91
126
|
},
|
|
92
127
|
{
|
|
93
128
|
id: 'searchProvider',
|
|
94
|
-
label: 'search
|
|
95
|
-
description: '
|
|
129
|
+
label: 'search engine',
|
|
130
|
+
description: 'Which engine backs web search. Exa and DuckDuckGo work with no setup; Brave needs '
|
|
131
|
+
+ 'a BRAVE_SEARCH_API_KEY in your environment',
|
|
96
132
|
// Display full engine names; the stored config value stays the short id.
|
|
97
133
|
values: SEARCH_PROVIDERS.map(p => SEARCH_PROVIDER_LABELS[p])
|
|
98
134
|
},
|
|
99
135
|
{
|
|
100
136
|
id: 'requestTimeoutMs',
|
|
101
137
|
label: 'command timeout',
|
|
102
|
-
description: '
|
|
103
|
-
+ '
|
|
104
|
-
+ '
|
|
105
|
-
+ '
|
|
106
|
-
+ 'the verify/fix gate children. off disables it everywhere — gates can then hang '
|
|
107
|
-
+ 'unbounded',
|
|
138
|
+
description: 'Give up on any single command that runs this long, and tell the model to set its '
|
|
139
|
+
+ 'own timeout next time. Stops a run from waiting forever on a dev server or a '
|
|
140
|
+
+ 'hung build. Covers the main session and the checking steps; "off" lets a stuck '
|
|
141
|
+
+ 'command hang the run until you notice',
|
|
108
142
|
// Display human labels; the stored config value stays the ms number.
|
|
109
143
|
values: COMMAND_TIMEOUT_OPTIONS.map(o => o.label)
|
|
110
144
|
},
|
|
111
145
|
{
|
|
112
146
|
id: 'streamInactivityMs',
|
|
113
|
-
label: '
|
|
114
|
-
description: '
|
|
115
|
-
+ '
|
|
116
|
-
+ '
|
|
117
|
-
+ '
|
|
118
|
-
+ '
|
|
119
|
-
+ 'generous on local backends: prompt processing on a large context legitimately '
|
|
120
|
-
+ 'emits nothing for minutes. off disables it on both the main session and children',
|
|
147
|
+
label: 'stuck reply retry',
|
|
148
|
+
description: 'Give up on a model reply that has sent nothing for this long and ask again. A '
|
|
149
|
+
+ 'dropped connection looks exactly like a model thinking hard and reports no '
|
|
150
|
+
+ 'error, so a run can sit dead for hours. Only total silence counts, and the clock '
|
|
151
|
+
+ 'pauses while a command runs — local models go quiet for minutes on long prompts, '
|
|
152
|
+
+ 'so leave room',
|
|
121
153
|
// Display human labels; the stored config value stays the ms number.
|
|
122
154
|
values: STREAM_INACTIVITY_OPTIONS.map(o => o.label)
|
|
123
155
|
},
|
|
124
156
|
{
|
|
125
157
|
id: 'yoloMode',
|
|
126
158
|
label: 'yolo mode',
|
|
127
|
-
description: '
|
|
128
|
-
+ 'and
|
|
129
|
-
+ '
|
|
130
|
-
+ '
|
|
131
|
-
+ '(YOLO) in the task file and debt ledger. For THROWAWAY/TEST projects you are '
|
|
132
|
-
+ 'not watching — a real run should decide these itself'
|
|
159
|
+
description: 'Stop asking you anything: every question takes the option pi recommends, a failed '
|
|
160
|
+
+ 'check is accepted and written down as debt, and a failed final check is retried '
|
|
161
|
+
+ 'until the budget runs out. Each auto-answer is marked (YOLO) in the task file. '
|
|
162
|
+
+ 'For throwaway projects you are not watching'
|
|
133
163
|
}
|
|
134
164
|
];
|
|
135
165
|
/** Human label for the stored command-timeout ms (falls back to the raw ms). */
|
|
@@ -162,10 +192,10 @@ export function extensionItems(extensions, whitelist) {
|
|
|
162
192
|
return extensions.map(e => ({
|
|
163
193
|
id: EXT_ID_PREFIX + e.path,
|
|
164
194
|
label: `ext: ${e.label}`,
|
|
165
|
-
description: `
|
|
166
|
-
+ '
|
|
167
|
-
+ '
|
|
168
|
-
+ `
|
|
195
|
+
description: `Load this ${e.origin} extension in the helper sessions pi-task spawns. They run `
|
|
196
|
+
+ 'with extensions off by default, so turn this on when the extension provides the '
|
|
197
|
+
+ 'model they need (pi-lmstudio, for example). They also inherit its tools and '
|
|
198
|
+
+ `hooks, so only enable ones you trust. ${e.path}`,
|
|
169
199
|
currentValue: whitelist.includes(e.path) ? 'on' : 'off',
|
|
170
200
|
values: ['on', 'off']
|
|
171
201
|
}));
|
|
@@ -175,15 +205,62 @@ export function applyExtensionToggle(whitelist, entryPath, on) {
|
|
|
175
205
|
const rest = whitelist.filter(p => p !== entryPath);
|
|
176
206
|
return on ? [...rest, entryPath] : rest;
|
|
177
207
|
}
|
|
208
|
+
/** Overlay width; the list gets `- 4` of it, the description `- 4` again. */
|
|
209
|
+
const OVERLAY_WIDTH = 68;
|
|
210
|
+
/** Settings rows shown at once before the list scrolls. */
|
|
211
|
+
const MAX_VISIBLE = 9;
|
|
212
|
+
/**
|
|
213
|
+
* Tallest body the settings list can render, so {@link BorderedBox} can pad
|
|
214
|
+
* every frame to it and hold the border still. Mirrors SettingsList's own
|
|
215
|
+
* layout: one pad row, the visible rows, the scroll counter, a blank, the
|
|
216
|
+
* selected item's wrapped description, a blank, the key hint.
|
|
217
|
+
*/
|
|
218
|
+
export function settingsBodyHeight(descriptions, maxVisible, wrapWidth) {
|
|
219
|
+
const tallestDescription = Math.max(0, ...descriptions.map(d => wrapTextWithAnsi(d, wrapWidth).length));
|
|
220
|
+
return 1 + maxVisible + 1 + 1 + tallestDescription + 1 + 1;
|
|
221
|
+
}
|
|
178
222
|
function makeTheme(theme) {
|
|
179
223
|
return {
|
|
180
|
-
label: (text, selected) =>
|
|
181
|
-
|
|
224
|
+
label: (text, selected) => selected ? theme.fg('accent', theme.bold(text)) : theme.fg('text', text),
|
|
225
|
+
// A filled/hollow dot makes the on/off column scannable at a glance
|
|
226
|
+
// without reading a word on every row. Enum values (an engine name, a
|
|
227
|
+
// duration) are real content, so they stay readable rather than muted.
|
|
228
|
+
value: (text, selected) => {
|
|
229
|
+
if (text === 'on')
|
|
230
|
+
return theme.fg('success', '● on');
|
|
231
|
+
if (text === 'off')
|
|
232
|
+
return theme.fg('dim', '○ off');
|
|
233
|
+
return theme.fg(selected ? 'accent' : 'muted', text);
|
|
234
|
+
},
|
|
182
235
|
description: text => theme.fg('muted', text),
|
|
183
|
-
|
|
236
|
+
// Two cells wide to match the unselected row's " " indent — a
|
|
237
|
+
// single-cell cursor shifts the selected label one column left.
|
|
238
|
+
cursor: theme.fg('accent', '❯') + ' ',
|
|
184
239
|
hint: text => theme.fg('dim', text)
|
|
185
240
|
};
|
|
186
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Builds the framed settings panel. Split out of the command handler so the
|
|
244
|
+
* exact component the overlay shows can be rendered to a string in a test or a
|
|
245
|
+
* preview script, rather than only being inspectable by opening the TUI.
|
|
246
|
+
*/
|
|
247
|
+
export function createSettingsPanel(items, theme, onChange, onCancel) {
|
|
248
|
+
const list = new SettingsList(items, MAX_VISIBLE, makeTheme(theme), onChange, onCancel);
|
|
249
|
+
return new BorderedBox(list, CONFIG_TITLE, s => theme.fg('borderMuted', s), s => theme.fg('accent', theme.bold(s)), settingsBodyHeight(items.map(i => i.description), MAX_VISIBLE, OVERLAY_WIDTH - 8));
|
|
250
|
+
}
|
|
251
|
+
/** The full settings row list for the current config, in menu order. */
|
|
252
|
+
export function panelItems(cfg, installed) {
|
|
253
|
+
return [
|
|
254
|
+
...ITEMS.map(({ id, label, description, values }) => ({
|
|
255
|
+
id: id,
|
|
256
|
+
label,
|
|
257
|
+
description,
|
|
258
|
+
currentValue: displayValue(cfg, id, Boolean(values)),
|
|
259
|
+
values: values ?? ['on', 'off']
|
|
260
|
+
})),
|
|
261
|
+
...extensionItems(installed, cfg.extensionWhitelist)
|
|
262
|
+
];
|
|
263
|
+
}
|
|
187
264
|
async function handleTaskConfig(_args, ctx) {
|
|
188
265
|
const cfg = { ...getConfig(), extensionWhitelist: [...getConfig().extensionWhitelist] };
|
|
189
266
|
// Enumerated live at open so an installed extension appears and an
|
|
@@ -199,50 +276,36 @@ async function handleTaskConfig(_args, ctx) {
|
|
|
199
276
|
ctx.ui.notify(lines.join(' | '), 'info');
|
|
200
277
|
return;
|
|
201
278
|
}
|
|
202
|
-
await ctx.ui.custom((_tui, theme, _kb, done) => {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
else if (id === 'streamInactivityMs') {
|
|
229
|
-
const opt = STREAM_INACTIVITY_OPTIONS.find(o => o.label === newValue);
|
|
230
|
-
if (opt)
|
|
231
|
-
cfg.streamInactivityMs = opt.ms;
|
|
232
|
-
}
|
|
233
|
-
else {
|
|
234
|
-
;
|
|
235
|
-
cfg[id] = newValue === 'on';
|
|
236
|
-
}
|
|
237
|
-
saveConfig(cfg).catch(() => { });
|
|
238
|
-
}, () => done(undefined));
|
|
239
|
-
return new BorderedBox(list, CONFIG_TITLE, s => theme.fg('borderMuted', s), s => theme.fg('accent', theme.bold(s)));
|
|
240
|
-
}, { overlay: true, overlayOptions: { width: 58 } });
|
|
279
|
+
await ctx.ui.custom((_tui, theme, _kb, done) => createSettingsPanel(panelItems(cfg, installed), theme, (id, newValue) => {
|
|
280
|
+
if (id.startsWith(EXT_ID_PREFIX)) {
|
|
281
|
+
cfg.extensionWhitelist = applyExtensionToggle(cfg.extensionWhitelist, id.slice(EXT_ID_PREFIX.length), newValue === 'on');
|
|
282
|
+
}
|
|
283
|
+
else if (id === 'searchProvider') {
|
|
284
|
+
const provider = providerForLabel(newValue);
|
|
285
|
+
if (provider)
|
|
286
|
+
cfg.searchProvider = provider;
|
|
287
|
+
}
|
|
288
|
+
else if (id === 'requestTimeoutMs') {
|
|
289
|
+
const opt = COMMAND_TIMEOUT_OPTIONS.find(o => o.label === newValue);
|
|
290
|
+
if (opt)
|
|
291
|
+
cfg.requestTimeoutMs = opt.ms;
|
|
292
|
+
}
|
|
293
|
+
else if (id === 'streamInactivityMs') {
|
|
294
|
+
const opt = STREAM_INACTIVITY_OPTIONS.find(o => o.label === newValue);
|
|
295
|
+
if (opt)
|
|
296
|
+
cfg.streamInactivityMs = opt.ms;
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
;
|
|
300
|
+
cfg[id] = newValue === 'on';
|
|
301
|
+
}
|
|
302
|
+
saveConfig(cfg).catch(() => { });
|
|
303
|
+
}, () => done(undefined)), { overlay: true, overlayOptions: { width: OVERLAY_WIDTH } });
|
|
241
304
|
}
|
|
242
305
|
export function registerConfig(pi) {
|
|
243
306
|
registerBridgeCommand(pi, 'task-config', {
|
|
244
|
-
description: 'Configure pi-task settings (remote
|
|
245
|
-
+ '
|
|
307
|
+
description: 'Configure pi-task settings (remote control, auto-commit, verify work, enforce '
|
|
308
|
+
+ 'guidelines, research, timeouts, extensions for helper sessions).',
|
|
246
309
|
handler: handleTaskConfig
|
|
247
310
|
});
|
|
248
311
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The installed pi-task version, read from package.json at runtime so nothing
|
|
3
|
+
* has to be regenerated on release. Two levels up holds for both src/<dir>
|
|
4
|
+
* (tests) and dist/<dir> (build), since tsc preserves the layout under rootDir.
|
|
5
|
+
* Falls back to '0.0.0' rather than throwing: every caller here is cosmetic
|
|
6
|
+
* (a User-Agent, a title bar) and none is worth failing over.
|
|
7
|
+
*/
|
|
8
|
+
export declare function readPkgVersion(): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
/**
|
|
5
|
+
* The installed pi-task version, read from package.json at runtime so nothing
|
|
6
|
+
* has to be regenerated on release. Two levels up holds for both src/<dir>
|
|
7
|
+
* (tests) and dist/<dir> (build), since tsc preserves the layout under rootDir.
|
|
8
|
+
* Falls back to '0.0.0' rather than throwing: every caller here is cosmetic
|
|
9
|
+
* (a User-Agent, a title bar) and none is worth failing over.
|
|
10
|
+
*/
|
|
11
|
+
export function readPkgVersion() {
|
|
12
|
+
try {
|
|
13
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const pkg = JSON.parse(readFileSync(join(here, '..', '..', 'package.json'), 'utf8'));
|
|
15
|
+
return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return '0.0.0';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
4
1
|
import { parseHTML } from 'linkedom';
|
|
5
2
|
import { Readability } from '@mozilla/readability';
|
|
6
3
|
import TurndownService from 'turndown';
|
|
4
|
+
import { readPkgVersion } from '../shared/pkg-version.js';
|
|
7
5
|
const turndown = new TurndownService({
|
|
8
6
|
codeBlockStyle: 'fenced',
|
|
9
7
|
headingStyle: 'atx',
|
|
@@ -33,21 +31,9 @@ export function cleanHtml(html, baseUrl) {
|
|
|
33
31
|
}
|
|
34
32
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
35
33
|
const DEFAULT_MAX_BYTES = 2 * 1024 * 1024; // 2 MB
|
|
34
|
+
// Read at runtime so the User-Agent never drifts out of sync with releases.
|
|
36
35
|
const PKG_VERSION = readPkgVersion();
|
|
37
36
|
const USER_AGENT = `pi-worker/${PKG_VERSION} (+https://npmjs.com/package/@mjasnikovs/pi-worker)`;
|
|
38
|
-
// Read the version from package.json at runtime so the User-Agent never drifts
|
|
39
|
-
// out of sync with releases. Two levels up holds for both src/workers (tests)
|
|
40
|
-
// and dist/workers (build) since tsc preserves the layout under rootDir.
|
|
41
|
-
function readPkgVersion() {
|
|
42
|
-
try {
|
|
43
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
44
|
-
const pkg = JSON.parse(readFileSync(join(here, '..', '..', 'package.json'), 'utf8'));
|
|
45
|
-
return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return '0.0.0';
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
37
|
// Decide how to handle a response based on its content-type. HTML is run through
|
|
52
38
|
// the readability/turndown pipeline; text-ish formats (markdown, plain text,
|
|
53
39
|
// JSON, XML/feeds) are already clean and pass through verbatim; binary formats
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.49",
|
|
4
4
|
"description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|