@giladbarnea/pi-user-agents 0.0.2 → 0.0.3

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/INTERNALS.md CHANGED
@@ -38,7 +38,7 @@ The shared runtime is extracted from the parent's `ModelRegistry` facade (`ctx.m
38
38
 
39
39
  ## Model name & alias resolution
40
40
 
41
- User-defined model aliases (`name` in `models.json` `modelOverrides`) take precedence: `-m opus` resolves to `claude-bridge/claude-opus-4-6` directly, without ambiguity. The extension checks for an exact name match before falling through to the SDK's `resolveCliModel`, which only checks `model.id` (never `model.name`) and otherwise falls back to substring matching — where "opus" collides with dozens of built-in models across the catalog.
41
+ User-defined model aliases (`name` in `models.json` `modelOverrides`) take precedence: `-m opus` resolves to `claude-bridge/claude-opus-4-6` directly, without ambiguity. Other model values must exactly match a live model ID or canonical `provider/id` reference before the extension calls the SDK's `resolveCliModel`. Partial and custom model IDs are rejected.
42
42
 
43
43
  > **Note (known issue):** `enabledModels` (i.e., “Scoped models”) currently have no special role in how the extension resolves models. They should be promoted to the same precedence level as user-defined model names in a future fix.
44
44
 
@@ -48,7 +48,7 @@ Exact names should be unique: duplicate aliases silently select the first model,
48
48
 
49
49
  ### Semantic command coloring
50
50
 
51
- While you type, the editor paints `/agent` lines from the same semantic scan that drives submission parsing (see `PARSER_SPEC.md` §11): the command in the theme's `accent`, recognized leading options in `syntaxKeyword`, and valid values (quotes included) in `syntaxString`. Invalid thinking levels, unresolved model IDs or aliases, and unknown provider or tool names use `error`, as do explicitly blocked Pi options such as `-c`/`--continue` and options typed after the task prose has begun — along with their values. Validation comes from Pi's argument parser, model resolver, and live catalogs rather than a second editor grammar. Prose, unknown dash-words, quoted spans, and escaped words stay plain. All colors come from the active Pi theme, so custom themes and hot reload apply automatically.
51
+ While you type, the editor paints `/agent` lines from the same semantic scan that drives submission parsing (see `PARSER_SPEC.md` §11): the command in the theme's `accent`, recognized leading options in `syntaxType`, and valid values (quotes included) in `syntaxString`. Invalid thinking levels, unresolved model IDs or aliases, and unknown provider or tool names use `error`, including blocked Pi options such as `-c`/`--continue` and `-p`/`--print`, plus options typed after the task prose has begun — along with their values. Validation comes from Pi's argument parser, model resolver, and live catalogs rather than a second editor grammar. Prose, unknown dash-words, quoted spans, and escaped words stay plain. All colors come from the active Pi theme, so custom themes and hot reload apply automatically.
52
52
 
53
53
  ### Eager argument completion
54
54
 
package/PARSER_SPEC.md CHANGED
@@ -45,9 +45,10 @@ leading whitespace (unchanged from today).
45
45
  | `-i`, `--isolate` | boolean | Consumed here; `isolate = true` |
46
46
  | `-j`, `--join` | boolean | Consumed here; `context = true` |
47
47
 
48
- `-m` and `--model` are one semantic option. Both resolve once through Pi's
49
- `resolveCliModel()` path, and a preceding `--provider` scopes either spelling. `-j` is the
50
- extension's join option. A model option with no value and no remaining task ends in the normal
48
+ `-m` and `--model` are one semantic option. Both require an exact live model ID, canonical
49
+ `provider/id` reference, or user-defined alias before resolving through Pi's `resolveCliModel()`
50
+ path. A preceding `--provider` scopes either spelling. `-j` is the extension's join option.
51
+ A model option with no value and no remaining task ends in the normal
51
52
  **usage error**.
52
53
 
53
54
  ### 3b. Known pi options — declared statically; added to `forwardedArgs`
@@ -59,8 +60,7 @@ extension's join option. A model option with no value and no remaining task ends
59
60
  **Boolean** (consume no value):
60
61
  `--resume`/`-r`, `--no-session`, `--no-tools`/`-nt`, `--no-builtin-tools`/`-nbt`,
61
62
  `--no-extensions`/`-ne`, `--no-skills`/`-ns`, `--no-prompt-templates`/`-np`, `--no-themes`,
62
- `--no-context-files`/`-nc`, `--verbose`, `--approve`/`-a`, `--no-approve`/`-na`, `--offline`,
63
- `--print`/`-p`.
63
+ `--no-context-files`/`-nc`, `--verbose`, `--approve`/`-a`, `--no-approve`/`-na`, `--offline`.
64
64
 
65
65
  Notes:
66
66
  - The shared declaration is a snapshot of `pi --help`; it is maintained by hand. There is no
@@ -77,7 +77,6 @@ Notes:
77
77
  The dequoted value is stored as **one element** in `forwardedArgs`
78
78
  (e.g. `["--system-prompt", "I am psyched"]`), so pi's `parseArgs` maps it as a single value.
79
79
  If a value opens with a quote but has no matching close, fall back to the whitespace-delimited token.
80
- - `--print`/`-p` is modeled as **boolean** here (pi's optional-value form would eat prose).
81
80
 
82
81
  Value examples:
83
82
  - `--system-prompt "I am psyched"` → value `I am psyched`.
@@ -86,8 +85,8 @@ Value examples:
86
85
  - `-m "gpt56s"` → `forwardedArgs` contains `--model`, `gpt56s`.
87
86
 
88
87
  ### 3c. Blocked pi options — rejected
89
- `-c`/`--continue`, `--theme <path>`, `--models`, `--export`, `--list-models`, `-h`/`--help`,
90
- `-v`/`--version`.
88
+ `-c`/`--continue`, `-p`/`--print`, `--theme <path>`, `--models`, `--export`, `--list-models`,
89
+ `-h`/`--help`, `-v`/`--version`.
91
90
  Appearing in **args mode** (leading) → hard **error**:
92
91
  `"/agent does not support <opt>; it would disrupt the background agent run."`
93
92
 
@@ -239,10 +238,9 @@ source spans:
239
238
  - `option` — a recognized, non-blocked leading option (extension-owned or forwarded);
240
239
  - `value` — the value consumed by a leading value-taking option. A quoted value's span
241
240
  includes its surrounding quotes;
242
- - `invalid-value` — a leading closed-set value rejected by the same canonical validation
243
- used at submission and session creation: Pi's argument parser for thinking levels, the
244
- child model resolver for model IDs and user-defined aliases, and the live provider and
245
- tool catalogs;
241
+ - `invalid-value` — a leading closed-set value rejected by the same validation used at
242
+ submission and session creation: Pi's argument parser for thinking levels, exact live model
243
+ IDs or user-defined aliases, and the live provider and tool catalogs;
246
244
  - `blocked` — a blocked option in args mode, plus the value a blocked value-taking option
247
245
  would consume (`--theme ./theme.json` yields two `blocked` tokens). Unlike
248
246
  `parseAgentCommand`, the scan never throws: it records the first blocked token and keeps
@@ -259,6 +257,6 @@ prose → usage error), so parsing and coloring cannot drift.
259
257
  cursor analysis (§10) and shifts token spans into editor-line coordinates.
260
258
 
261
259
  The editor coloring layer (`editor-coloring.ts`) maps token semantics onto Pi theme
262
- variables — command `accent`, options `syntaxKeyword`, valid values `syntaxString`, and
260
+ variables — command `accent`, options `syntaxType`, valid values `syntaxString`, and
263
261
  invalid, blocked, or misplaced tokens `error` — by repainting the laid-out chunk text after
264
262
  word-wrap layout, so editing, wrapping, cursor behavior, and autocomplete are untouched.
package/README.md CHANGED
@@ -36,17 +36,26 @@ The first preserves the session's knowledge before the context window fills, wit
36
36
 
37
37
  ### 2. Watch
38
38
 
39
- Every agent gets a row in a widget under the editor: status, task, live activity, and a one-cell context meter.
39
+ Every agent gets a row in a widget under the editor: status, task, model, turn count, tool uses, elapsed time, a one-cell context meter, and the last line of live activity.
40
40
 
41
- ```text
42
- ✓ User agents
43
- ↑↓ select · Enter view · x dispose · Esc back
44
- └─ ⏺ ✓ /agent ▇ i am testing the widget now - read a ton...
45
- ⎿ Loaded several large Pi source and documentation...
46
- ```
41
+ <a href="screenshots/below-editor-widget.png">
42
+ <img src="screenshots/below-editor-widget.png" width="100%" alt="The user agents widget under the editor, showing one finished agent and its latest output">
43
+ </a>
44
+
45
+ <div align="center"><em>The widget sits under the editor and stays out of your way.</em></div>
46
+
47
+ <br>
47
48
 
48
49
  Press `←` or `↓` from the editor to focus the widget, pick an agent, press `Enter`, and its overlay opens: the full conversation, streaming live, following the tail as it grows. Scroll up to pause following; `End` resumes it. Tool calls render as proper views — `read`, `edit`, `write`, `grep`, `find`, `ls`, and `bash` each get a dedicated format, everything else a readable generic one.
49
50
 
51
+ <a href="screenshots/overlay.png">
52
+ <img src="screenshots/overlay.png" width="100%" alt="The agent overlay: a rendered bash call, its output, a todo update, and the final response, over the dimmed main conversation">
53
+ </a>
54
+
55
+ <div align="center"><em>The overlay, over a main conversation that never paused.</em></div>
56
+
57
+ <br>
58
+
50
59
  The context meter is the agent's own footer gauge in one cell: it fills `▁▂▃▄▅▆▇█` against that agent's model context window and shifts color through the same stages as Pi's footer (dim, then muted at 40%, warning at 65%, error at 85%).
51
60
 
52
61
  ### 3. Steer
@@ -114,7 +123,7 @@ Everything goes through one command: `/agent [options] <task>`.
114
123
  ## Good to know
115
124
 
116
125
  - Result cards persist across session reloads; live agent sessions don't. After a reload you keep every card, not the steerable sessions.
117
- - Some accepted `pi` options have no effect on a background run (the session, approval, offline, API-key, and print families). They parse; they just don't do anything yet.
126
+ - Some accepted `pi` options have no effect on a background run (the session, approval, offline, and API-key families). They parse; they just don't do anything yet.
118
127
  - `c` copies via `pbcopy`, so it's macOS-only for now.
119
128
  - The overlay caps very large tool outputs and omits thinking entries.
120
129
 
package/command-line.ts CHANGED
@@ -292,13 +292,13 @@ function booleanOptionsWithoutChildEffect(): AgentOptionDefinition[] {
292
292
  legacyBooleanOption("approve", ["--approve", "-a"]),
293
293
  legacyBooleanOption("no-approve", ["--no-approve", "-na"]),
294
294
  legacyBooleanOption("offline", ["--offline"]),
295
- legacyBooleanOption("print", ["--print", "-p"]),
296
295
  ];
297
296
  }
298
297
 
299
298
  function blockedOptions(): AgentOptionDefinition[] {
300
299
  return [
301
300
  blockedOption("continue", ["-c", "--continue"]),
301
+ blockedOption("print", ["-p", "--print"]),
302
302
  blockedOption("theme", ["--theme"], "value"),
303
303
  blockedOption("models", ["--models"]),
304
304
  blockedOption("export", ["--export"]),
@@ -11,7 +11,7 @@ export type StyleTheme = { fg(color: ThemeColor, text: string): string };
11
11
 
12
12
  export const AGENT_SEMANTIC_COLORS: Record<AgentTokenSemantic | "command", ThemeColor> = {
13
13
  command: "accent",
14
- option: "syntaxKeyword",
14
+ option: "syntaxType",
15
15
  value: "syntaxString",
16
16
  "invalid-value": "error",
17
17
  blocked: "error",
@@ -36,7 +36,7 @@ type ColoredEditor = ColoringRuntimeEditor & { [COLORED_EDITOR]?: true };
36
36
  * Map an editor line to theme-colored spans via the shared semantic scan.
37
37
  * Returns undefined for lines that are not agent commands.
38
38
  *
39
- * @example computeAgentColorSpans("/agent -m gpt56s go")?.[1] // { start: 7, end: 9, color: "syntaxKeyword" }
39
+ * @example computeAgentColorSpans("/agent -m gpt56s go")?.[1] // { start: 7, end: 9, color: "syntaxType" }
40
40
  */
41
41
  export function computeAgentColorSpans(
42
42
  line: string,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@giladbarnea/pi-user-agents",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Run, view, and control background Pi agents.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -23,6 +23,7 @@
23
23
  "files": [
24
24
  "*.ts",
25
25
  "tool-render",
26
+ "screenshots",
26
27
  "README.md",
27
28
  "PARSER_SPEC.md",
28
29
  "INTERNALS.md",
package/runner.ts CHANGED
@@ -239,6 +239,30 @@ export function createAgentValueValidator(
239
239
  };
240
240
  }
241
241
 
242
+ /**
243
+ * Return whether a model value exactly names a live ID, canonical reference, or user alias.
244
+ *
245
+ * @example isExactCatalogModelReference([{ provider: "openai", id: "gpt-5" } as Model], "openai/gpt-5") // true
246
+ */
247
+ function isExactCatalogModelReference(
248
+ models: readonly Model[],
249
+ value: string,
250
+ provider: string | undefined,
251
+ ): boolean {
252
+ const normalizedValue = value.toLowerCase();
253
+ const normalizedProvider = provider?.toLowerCase();
254
+ const exactAliasMatch = models.some(
255
+ (model) => model.name?.toLowerCase() === normalizedValue,
256
+ );
257
+ const exactReferenceMatch = models.some(
258
+ (model) =>
259
+ (normalizedProvider === undefined || model.provider.toLowerCase() === normalizedProvider) &&
260
+ (model.id.toLowerCase() === normalizedValue ||
261
+ `${model.provider}/${model.id}`.toLowerCase() === normalizedValue),
262
+ );
263
+ return exactAliasMatch || exactReferenceMatch;
264
+ }
265
+
242
266
  /** Resolve forwarded runtime options against the child session's model runtime. */
243
267
  export function resolveForwardedOptions(
244
268
  parsed: Args,
@@ -246,6 +270,10 @@ export function resolveForwardedOptions(
246
270
  ): ForwardedOptions {
247
271
  const forwarded: ForwardedOptions = {};
248
272
  if (parsed.model) {
273
+ if (!isExactCatalogModelReference(modelRuntime.getModels(), parsed.model, parsed.provider)) {
274
+ const display = parsed.provider ? `${parsed.provider}/${parsed.model}` : parsed.model;
275
+ throw new Error(`Model "${display}" not found in the live model catalog.`);
276
+ }
249
277
  const nameMatch = modelRuntime
250
278
  .getModels()
251
279
  .find((m) => m.name?.toLowerCase() === parsed.model!.toLowerCase());
Binary file