Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions apps/desktop/src/components/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
normalizeLargePasteThreshold,
PERMISSION_MODES,
restoreInlineComposerFileReferenceTokens,
rewriteIdeographicCommaTrigger,
serializeComposerFileReferences,
serializeInlineComposerFileReferences,
stripInlineComposerFileReferenceTokens,
Expand Down Expand Up @@ -2337,10 +2338,26 @@ export function Composer({
}}
onInput={(e) => {
const el = e.currentTarget;
const nextValue = readEditorValue(el);
const source = readEditorValue(el);
const { start } = editorSelectionRange(el);
// A Chinese IME commits "、" where an ASCII "/" is meant, and
// a leading "/" is what makes the slash menu reachable
// without switching input methods (D405). Only the first
// character of a draft that was empty is rewritten, so the
// mark stays ordinary punctuation anywhere else.
const nextValue =
valueRef.current === ""
? rewriteIdeographicCommaTrigger(source)
: source;
invalidatePromptEnhancement();
editorValueRef.current = nextValue;
if (nextValue === source) {
editorValueRef.current = nextValue;
} else {
// Leave editorValueRef stale so the layout effect repaints
// the editable with the substituted "/" and puts the caret
// back after it.
pendingEditorCaretRef.current = start;
}
valueRef.current = nextValue;
setValue(nextValue);
// `filter` allocates even when it drops nothing, and a new
Expand Down
22 changes: 22 additions & 0 deletions apps/desktop/test/composer-ime.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,25 @@ test("model menu keydown ignores IME composition keystrokes", () => {
"menu navigation must bail out while an IME composition is active",
);
});

test("an ideographic comma opens the slash menu from an empty draft (D405)", () => {
const handler = composerSource.slice(
composerSource.indexOf("onInput={(e) => {"),
composerSource.indexOf("onCompositionStart={() => setComposing(true)}"),
);
assert.match(
handler,
/rewriteIdeographicCommaTrigger\(/,
"the editable must route a committed 、 through the shared rewrite",
);
assert.match(
handler,
/valueRef\.current === ""/,
"only a draft with nothing in it may be rewritten",
);
assert.match(
handler,
/pendingEditorCaretRef\.current = start/,
"the caret must land after the substituted slash",
);
});
63 changes: 63 additions & 0 deletions docs/adr/0230-skill-ships-with-the-agent-core-tool-set.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# ADR 0230: Skill Ships with the Agent Core Tool Set

- Status: Accepted
- Date: 2026-09-11
- Deciders: PI-Desktop runtime maintainers
- Amends: D174, ADR 0048, ADR 0219

## Context

ADR 0048 keeps the first Agent request on a small core set and defers every
other capability behind the local `ToolSearch` tool, so a large plugin surface
cannot recreate the original prompt bloat. `Skill` was registered into that
deferred set: it only appears in the provider schema after the model searches
for it.

Two later decisions assume a `Skill` tool the model can call immediately:

- D174 makes the skill catalog the model-invoked way to load a document, and
advertises it in the `# Skills` system-prompt section with an instruction to
load a matching skill first.
- ADR 0219 answers a user-typed `/skill-id` by persisting an instruction to
call the local `Skill` tool with the validated id on that turn.

Neither can be satisfied when the tool is absent from the first request's tool
list. A model that does not search for it either re-searches, calls a tool it
cannot see, or answers from the catalog line alone; a user who explicitly asked
for a skill pays one or two extra round trips before the body is ever loaded.

## Decision

1. `Skill` joins `AGENT_CORE_TOOL_NAMES`, so an Agent-mode request carries its
schema from the first turn. Its registration gate is unchanged: the tool
exists only when the skill catalog is non-empty, and Plan and Goal continue
to omit it entirely.
2. The tool stays out of the deferred catalog and therefore never appears under
`# On-demand tools`. Other on-demand capabilities (`BrowserPreview`, plugin
tools, plugin-development helpers, and MCP tools) keep their lazy behavior,
and `ToolSearch` is still registered whenever any of them exist.
3. No protocol, storage, permission, or skill-body change follows from this.
Bodies still load on demand through the same local tool and its existing
host path and permission checks.

## Consequences

- A matching task loads its skill on the first turn instead of discovering the
tool first, and a `/skill-id` invocation works as ADR 0219 describes.
- Every Agent-mode request carries one more tool schema. The catalog is already
bounded per session, and the delegation lifecycle was admitted to the core set
for the same reason: a capability the model has to go looking for is one it
will not use.
- Removing the `Skill` row from the deferred catalog also removes the only path
that could route a skill through `ToolSearch`, so the catalog description for
it is deleted rather than left unreachable.

## Alternatives considered

- **Keep `Skill` deferred and rely on prompting:** rejected because a tool
absent from the schema cannot be called no matter what the system prompt says,
which is the behavior that produced this issue.
- **Inject skill bodies into the system prompt:** rejected for the cost and
reload semantics D174 and ADR 0039 already settled.
- **Make every on-demand tool core:** rejected because it recreates the prompt
bloat ADR 0048 exists to prevent. Only `Skill` is admitted here.
52 changes: 52 additions & 0 deletions docs/adr/0231-ideographic-comma-opens-the-slash-menu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# ADR 0231: Ideographic Comma Opens the Composer Slash Menu

- Status: Accepted
- Date: 2026-09-11
- Deciders: PI-Desktop desktop UI maintainers
- Amends: D123, D139, ADR 0024

## Context

The composer `/` menu mirrors the pi CLI grammar: the trigger is the ASCII `/`
as the very first character of the draft. A Chinese IME produces `、` (U+3001,
ideographic comma) for that keystroke, so a user writing in Chinese has to
switch to ASCII input before they can reach the command menu, then switch back.
The menu already ignores in-flight IME composition (D139), so the missing piece
is only the alias itself.

Every other trigger character in the grammar is deliberate: `@` opens the file
menu, and a `/` anywhere but the first character is ordinary prose. The alias
must not turn legitimate punctuation into commands.

## Decision

1. A `、` committed as the first character of an empty composer draft is
rewritten to `/` before trigger detection runs. Afterwards the draft is an
ordinary slash invocation: the same menu opens, the same filtering applies,
and the same send path handles it.
2. Only that position is rewritten. The composer requires the draft to have been
empty before the keystroke, so a `、` that appears later — including at the
start of a draft that already holds text — stays ordinary punctuation.
3. The rewrite is a pure string function in the shared composer-trigger module,
unit tested next to the trigger grammar it feeds. The `@` file menu is
unaffected.

## Consequences

- A Chinese IME user reaches `/new`, `/compact`, the mode aliases, template
commands, plugin commands, and Skills without leaving the input method.
- A message that genuinely starts with `、` in an empty composer is rewritten.
The menu opens with `/` and the user can keep typing prose, which keeps the
substitution visible instead of silently altering text mid-sentence.
- No IPC, storage, or autocomplete-source change is required.

## Alternatives considered

- **Accept `、` as an additional trigger character in `detectTrigger`:** rejected
because the draft would keep a character the send path and transcript chip
would then have to understand, and the menu would open on a mark the pi CLI
grammar does not define.
- **Rewrite on every `、`, not just the first character:** rejected because it
would corrupt ordinary prose, where `、` is the standard list separator.
- **A toolbar button for commands:** rejected as a duplicate entry point to a
menu that already exists (see D123).
2 changes: 2 additions & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,5 @@ Each ADR includes:
| 0227 | Project group manual ordering | Accepted (amended by 0228) |
| 0228 | Long-press the project title to reorder | Accepted (amended by 0229) |
| 0229 | Press-and-move project title reorder | Accepted (amends 0228) |
| 0230 | Skill ships with the Agent core tool set | Accepted (amends D174 / ADR 0048 / ADR 0219; issue #204) |
| 0231 | Ideographic comma opens the composer slash menu | Accepted (amends D123 / D139 / ADR 0024; issue #65) |
5 changes: 4 additions & 1 deletion docs/spec/03-runtime/02-agent-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -890,14 +890,17 @@ the mode's core set plus any deferred tools that can be restored from successful
activation evidence still present in the effective session context:

- Agent: `Read`, `Bash`, `Edit`, and `Write` (matching pi's coding-agent core)
- Agent: `Skill` whenever the skill catalog is non-empty (D404, ADR 0230) — the
`# Skills` section and a user-typed `/skill-id` both ask the model to call
it, and a tool that is missing from the schema cannot be called at all
- Agent: `Task`, `TaskWait`, `TaskList`, and `TaskStop` as well, whenever the
subagent catalog is non-empty (§5f) — a capability the model has to go
looking for is one it will not use, and the delegation lifecycle is worth
the extra schemas per request
- Plan: `Read`, `Glob`, `Grep`, `BrowserPreview`, and `Bash`
- both modes: `ToolSearch` when at least one deferred capability exists

In Agent mode, `Glob` and `Grep` join `BrowserPreview`, plugin tools, `Skill`,
In Agent mode, `Glob` and `Grep` join `BrowserPreview`, plugin tools,
and plugin-development helpers in the deferred set. Both contract modes keep
their read/inspection core available, while the kind's submit tool
(`SubmitPlan` or `SubmitGoal`) is exposed only during the planning state, and
Expand Down
8 changes: 5 additions & 3 deletions docs/spec/03-runtime/03-tools-and-permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,16 @@ Let the agent get things done, but stay under control by default.

Following pi's coding-agent default, the first Agent request activates only
`Read`, `Bash`, `Edit`, and `Write`; `Glob` and `Grep` are loaded on demand.
Plan and Goal keep their read/inspection core. The runtime also registers capabilities
without sending their full schemas up front:
Plan and Goal keep their read/inspection core. `Skill` is deliberately not
deferred: a `/skill-id` invocation instructs the model to call it, and a tool
absent from the schema cannot be called at all, so it ships with the first
request whenever the skill catalog is non-empty (D404, ADR 0230). The runtime
also registers capabilities without sending their full schemas up front:

- `Glob` and `Grep` in Agent mode
- `BrowserPreview`
- `PluginCheck`, `PluginScaffold`, and `PluginPack`
- plugin-declared agent tools
- `Skill` when an enabled plugin contributes skills

These tools appear in a bounded `# On-demand tools` catalog with compact
descriptions. The model calls the local `ToolSearch` tool with an exact name or
Expand Down
12 changes: 12 additions & 0 deletions docs/spec/04-ux/04-builtin-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,15 @@ model to call the existing `Skill` tool with the validated id before answering.
Only Skills active for the current project are listed or accepted, so project
scope and plugin activation remain enforced at send time. If the Skill is no
longer active, the text follows the normal unknown-slash prompt path.

## 9. Ideographic comma opens the slash menu (D405)

A Chinese IME produces `、` (U+3001) where the ASCII `/` is meant, so reaching
the menu otherwise means switching input methods mid-sentence. When the composer
is empty, a committed `、` as its first character is rewritten to `/` before
trigger detection runs, and the ordinary slash menu opens with the same
insertion, filtering, and send behavior described above.

Only the first character of an empty draft is rewritten. A `、` anywhere else in
the draft is ordinary punctuation and is never touched, and the alias has no
effect on the `@` file menu.
60 changes: 60 additions & 0 deletions docs/spec/06-delivery/04-e2e-test-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -6318,6 +6318,9 @@ Each scenario is documented in this format:
| Security | E2E-028, E2E-029, E2E-030, E2E-024J, E2E-024K, E2E-024M, E2E-049, E2E-068, E2E-086, E2E-102c, E2E-102d, E2E-102e, E2E-105, E2E-106, E2E-107, E2E-108, E2E-109, E2E-110, E2E-112, E2E-113, E2E-115, E2E-116, E2E-117, E2E-119, E2E-121, E2E-122, E2E-123, E2E-142, E2E-148, E2E-151, E2E-153, E2E-158, E2E-187, E2E-196c, E2E-196b, E2E-196 |
| Quality | E2E-032, E2E-033, E2E-039, E2E-043, E2E-044, E2E-045, E2E-046, E2E-047, E2E-048, E2E-048A, E2E-049, E2E-050, E2E-053, E2E-055, E2E-056, E2E-057, E2E-058, E2E-059, E2E-060, E2E-061, E2E-062, E2E-063, E2E-064, E2E-065, E2E-066, E2E-067, E2E-068, E2E-069, E2E-070, E2E-071, E2E-072, E2E-073, E2E-074, E2E-075, E2E-076, E2E-077, E2E-078, E2E-079, E2E-080, E2E-081, E2E-082, E2E-083, E2E-084, E2E-085, E2E-086, E2E-092, E2E-093, E2E-094, E2E-095, E2E-096, E2E-097, E2E-098, E2E-099, E2E-100, E2E-101, E2E-102, E2E-102a, E2E-102b, E2E-102c, E2E-102d, E2E-102e, E2E-103, E2E-AGENTS-001, E2E-021a, E2E-024N, E2E-059a, E2E-060b, E2E-060c, E2E-061a, E2E-073a, E2E-111, E2E-114, E2E-117, E2E-118, E2E-119, E2E-120, E2E-122, E2E-123, E2E-142, E2E-143, E2E-144, E2E-145, E2E-146, E2E-147, E2E-148, E2E-150, E2E-151, E2E-153, E2E-155, E2E-158, E2E-159, E2E-160, E2E-161, E2E-162, E2E-163, E2E-168, E2E-172, E2E-173, E2E-174, E2E-011g, E2E-176, E2E-177, E2E-178, E2E-179, E2E-180, E2E-181, E2E-182, E2E-183, E2E-186, E2E-187, E2E-194, E2E-195, E2E-196a, E2E-196b, E2E-196c, E2E-198, E2E-199, E2E-200, E2E-196, E2E-201, E2E-204, E2E-202, E2E-203, E2E-205, E2E-206, E2E-207, E2E-208, E2E-209, E2E-210, E2E-218, E2E-219, E2E-250, E2E-252, E2E-102i |
| Quality (project ordering) | E2E-253 |
| C — Conversation & stream (IME slash alias) | E2E-255 |
| E — Tools & permissions (Skill residency) | E2E-254 |
| Quality (Skill residency and IME slash alias) | E2E-254, E2E-255 |

| Milestone | Scenarios |
|---|---|
Expand All @@ -6327,6 +6330,8 @@ Each scenario is documented in this format:
| M4 | E2E-022, E2E-023, E2E-024, E2E-025, E2E-026, E2E-030, E2E-038 |
| M5 | E2E-008a, E2E-032, E2E-033, E2E-034, E2E-039, E2E-043, E2E-044, E2E-045, E2E-046, E2E-047, E2E-048, E2E-048A, E2E-049, E2E-050, E2E-051, E2E-052, E2E-053, E2E-054, E2E-055, E2E-056, E2E-057, E2E-058, E2E-059, E2E-060, E2E-061, E2E-062, E2E-063, E2E-064, E2E-065, E2E-066, E2E-067, E2E-068, E2E-069, E2E-070, E2E-071, E2E-072, E2E-073, E2E-074, E2E-075, E2E-076, E2E-077, E2E-078, E2E-079, E2E-080, E2E-081, E2E-082, E2E-083, E2E-084, E2E-085, E2E-086, E2E-092, E2E-093, E2E-096, E2E-097, E2E-098, E2E-099, E2E-100, E2E-101, E2E-102, E2E-102a, E2E-102b, E2E-102c, E2E-102d, E2E-102e, E2E-AGENTS-001, E2E-059a, E2E-060b, E2E-060c, E2E-061a, E2E-073a, E2E-094, E2E-095, E2E-143, E2E-145, E2E-146, E2E-146a, E2E-147, E2E-177, E2E-178, E2E-180, E2E-181, E2E-182, E2E-183, E2E-186, E2E-187, E2E-194, E2E-195, E2E-204, E2E-208, E2E-250, E2E-252, E2E-102i |
| M5 (project ordering) | E2E-253 |
| M2 (IME slash alias) | E2E-255 |
| M5 (Skill residency) | E2E-254 |
| M6 | E2E-104, E2E-105, E2E-106, E2E-107, E2E-108, E2E-109, E2E-110, E2E-111, E2E-112, E2E-113, E2E-114, E2E-115, E2E-116, E2E-117, E2E-118, E2E-119, E2E-120, E2E-103, E2E-172 |
| M6+ | E2E-121, E2E-122, E2E-148, E2E-150, E2E-151, E2E-154, E2E-155, E2E-158, E2E-159, E2E-160, E2E-161, E2E-162, E2E-163, E2E-166, E2E-168, E2E-173, E2E-174, E2E-176, E2E-179, E2E-196a, E2E-196b, E2E-196c, E2E-198, E2E-199, E2E-200, E2E-202, E2E-203, E2E-205, E2E-209, E2E-210, E2E-212, E2E-213, E2E-214, E2E-215, E2E-216, E2E-217, E2E-218, E2E-219 |
| Post-MVP | E2E-022A, E2E-022B, E2E-022C, E2E-024I, E2E-024J, E2E-024K, E2E-024L, E2E-024M (plugin roadmap R2/R3/R6) |
Expand Down Expand Up @@ -10105,3 +10110,58 @@ sample extensions under `apps/desktop/test/fixtures/pi-extensions/`.
- **Status**: Source-contract covered (`app-store-sidebar.test.mjs`,
`sidebar-preferences.test.mjs`, `sidebar-project-reorder.test.mjs`);
rendered desktop journey Draft

#### E2E-254: A skill loads on the first Agent turn

- **Preconditions**: At least one Skill is active for the current project, a
provider is configured, and the session runs in Agent mode with another
on-demand capability present (for example `BrowserPreview` or a plugin tool).
- **Steps**:
1. Open a new Agent conversation and send a prompt that matches the active
Skill's description.
2. Inspect the first provider request and its tool list.
3. Confirm the model calls `Skill` with the exact id without calling
`ToolSearch` first, and that the returned document is the skill body.
4. Send `/<skill-id>` from the composer and inspect the following turn.
5. Switch the session to Plan mode and inspect the tool list again.
6. Disable or remove every Skill and start another Agent turn.
- **Expected**: Whenever the skill catalog is non-empty, `Skill` ships with the
first request and never appears under `# On-demand tools`, so both a matching
task and a `/skill-id` invocation load the body without a discovery round
trip. `ToolSearch` still exists for the other on-demand capabilities and
never returns `Skill`. Plan mode omits the tool and the `# Skills` section,
and an empty catalog registers no `Skill` tool at all.
- **Specs linked**: `03-runtime/02-agent-runtime.md` (§7.1),
`03-runtime/03-tools-and-permissions.md` (§2.1),
`04-ux/04-builtin-commands.md` (§8), `08-meta/decisions-log.md` (D404),
ADR 0048, ADR 0219, ADR 0230
- **Acceptance**: C (conversation & stream), E (tools & permissions), Quality
- **Milestone**: M5
- **Status**: Unit-covered (`packages/agent-runtime/src/runtime.test.ts`);
rendered desktop journey Draft
(do not run E2E locally unless explicitly requested)

#### E2E-255: An ideographic comma opens the slash menu

- **Preconditions**: A Chinese IME is available, the composer draft is empty,
and at least one slash entry exists (builtin alias, template, plugin command,
or Skill).
- **Steps**:
1. With the draft empty, type `、` and inspect the composer.
2. Continue typing a command name and accept the highlighted row.
3. Type a draft that contains `、` between other characters.
4. Send a draft whose first character was typed as `、` without accepting any
row.
- **Expected**: The committed `、` is rewritten to `/` in place, the ordinary
slash menu opens with the same filtering and keyboard behavior as a typed
`/`, and the caret stays after the substituted character. A `、` anywhere
later in the draft stays untouched text, and the `@` file menu never reacts
to the mark.
- **Specs linked**: `04-ux/04-builtin-commands.md` (§9),
`04-ux/08-component-spec.md` (§11), `08-meta/decisions-log.md` (D405),
ADR 0024, ADR 0231
- **Acceptance**: C (conversation & stream), Localization, Quality
- **Milestone**: M2
- **Status**: Unit-covered (`packages/shared/src/composer-trigger.test.ts`,
`apps/desktop/test/composer-ime.test.mjs`); rendered desktop journey Draft
(do not run E2E locally unless explicitly requested)
Loading
Loading