osdy-pi 1.4.1 → 1.5.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/LICENSE +5 -0
- package/README.md +46 -18
- package/extensions/osdy-pi/codex-usage-ui.ts +2 -32
- package/extensions/osdy-pi/locales/de.json +16 -0
- package/extensions/osdy-pi/locales/en.json +15 -0
- package/extensions/osdy-pi/locales/es.json +16 -0
- package/extensions/osdy-pi/locales/fr.json +16 -0
- package/extensions/osdy-pi/locales/pt-BR.json +16 -0
- package/extensions/osdy-pi/locales/pt.json +16 -0
- package/extensions/osdy-pi/locales/ru.json +16 -0
- package/extensions/osdy-pi/locales/uk.json +16 -0
- package/extensions/osdy-pi/locales/zh.json +16 -0
- package/extensions/osdy-pi/message-role-markers.test.ts +161 -0
- package/extensions/osdy-pi/message-role-markers.ts +52 -0
- package/extensions/osdy-pi/modal-frame.test.ts +78 -0
- package/extensions/osdy-pi/modal-frame.ts +35 -0
- package/extensions/osdy-pi/runtime.test.ts +25 -26
- package/extensions/osdy-pi/runtime.ts +10 -0
- package/extensions/osdy-pi/todo-command.test.ts +132 -0
- package/extensions/osdy-pi/todo-command.ts +60 -0
- package/extensions/osdy-pi/todo-config.test.ts +67 -0
- package/extensions/osdy-pi/todo-config.ts +78 -0
- package/extensions/osdy-pi/todo-domain.test.ts +106 -0
- package/extensions/osdy-pi/todo-domain.ts +183 -0
- package/extensions/osdy-pi/todo-i18n.test.ts +52 -0
- package/extensions/osdy-pi/todo-i18n.ts +43 -0
- package/extensions/osdy-pi/todo-panel.test.ts +163 -0
- package/extensions/osdy-pi/todo-panel.ts +88 -0
- package/extensions/osdy-pi/todo-session.test.ts +81 -0
- package/extensions/osdy-pi/todo-session.ts +75 -0
- package/extensions/osdy-pi/todo-tool-render.test.ts +63 -0
- package/extensions/osdy-pi/todo-tool-render.ts +60 -0
- package/extensions/osdy-pi/todo-tool.test.ts +139 -0
- package/extensions/osdy-pi/todo-tool.ts +104 -0
- package/extensions/osdy-pi/todo-widget.test.ts +235 -0
- package/extensions/osdy-pi/todo-widget.ts +111 -0
- package/extensions/osdy-pi/types.ts +1 -0
- package/extensions/osdy-pi/working-animation.test.ts +79 -14
- package/extensions/osdy-pi/working-animation.ts +8 -2
- package/package.json +11 -2
- package/themes/osdy-pi-catppuccin-frappe.json +1 -1
- package/themes/osdy-pi-catppuccin-latte.json +1 -1
- package/themes/osdy-pi-catppuccin-macchiato.json +1 -1
- package/themes/osdy-pi-catppuccin-mocha.json +1 -1
- package/themes/osdy-pi-dark.json +1 -1
- package/themes/osdy-pi-dracula.json +1 -1
- package/themes/osdy-pi-kanagawa-dragon.json +1 -1
- package/themes/osdy-pi-kanagawa-lotus.json +1 -1
- package/themes/osdy-pi-kanagawa-wave.json +1 -1
- package/themes/osdy-pi-lucent-orange.json +1 -1
- package/themes/osdy-pi-matrix.json +1 -1
- package/themes/osdy-pi-new.json +1 -1
- package/themes/osdy-pi-sexy.json +1 -1
- package/themes/osdy-pi-tokyo-night.json +1 -1
package/LICENSE
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
3
|
Copyright (c) 2026 Osdy
|
|
4
|
+
Copyright (c) 2026 juicesharp
|
|
5
|
+
|
|
6
|
+
TODO portions are adapted from @juicesharp/rpiv-todo@2.11.0
|
|
7
|
+
(https://github.com/juicesharp/rpiv-mono/tree/main/packages/rpiv-todo),
|
|
8
|
+
licensed under the MIT terms below.
|
|
4
9
|
|
|
5
10
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
11
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
|
@@ -8,10 +8,11 @@ Osdy Pi gives [Pi](https://github.com/earendil-works/pi) a themed, responsive te
|
|
|
8
8
|
|
|
9
9
|
| Area | What ships |
|
|
10
10
|
| --- | --- |
|
|
11
|
-
| Message cards | Pi-native
|
|
11
|
+
| Message cards | Pi-native colored user cards without emojis and plain assistant Markdown preceded by a separate `🦝` transcript row. |
|
|
12
12
|
| Header and mascot | A theme-aware `neon` header and selectable `bts` mascot, both responsive and persisted independently. |
|
|
13
13
|
| Accounts | `/osdy-account` switches Codex profiles in place with atomic activation, rollback, bounded auth files, and process-safe locks. |
|
|
14
14
|
| Quota | `/usage` and compact bars emphasize remaining Codex quota at warning (40% or less) and error (15% or less) thresholds. |
|
|
15
|
+
| TODO (starting in 1.5.0) | First-party `todo` snapshots track the Pi session branch; `/todos` reads them without editing. ODD Markdown remains separate. |
|
|
15
16
|
|
|
16
17
|
## Prerequisites
|
|
17
18
|
|
|
@@ -33,14 +34,7 @@ Osdy Pi gives [Pi](https://github.com/earendil-works/pi) a themed, responsive te
|
|
|
33
34
|
pi install git:github.com/OsdyOrtiz/Osdy-Pi
|
|
34
35
|
```
|
|
35
36
|
|
|
36
|
-
Already
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
pi install npm:osdy-pi@1.4.1
|
|
40
|
-
pi list
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Check that `pi list` shows `npm:osdy-pi@1.4.1`, then restart Pi or run `/reload`. `pi update` does not move a pinned npm version. Installing Osdy does not automatically install Joker; use the [explicit agents setup](#explicit-joker-agents-setup-in-normal-pi) after updating if you want Joker in normal Pi.
|
|
37
|
+
Already using a pinned npm version? `pi update` does not move a pinned version. First-party TODO is included starting in 1.5.0; until that version is published, npm `latest` may still resolve to an earlier release. Check the resolved version or use a checkout to evaluate 1.5.0 before publication. Installing Osdy does not automatically install Joker; see [explicit agents setup](#explicit-joker-agents-setup-in-normal-pi).
|
|
44
38
|
|
|
45
39
|
2. Start Pi:
|
|
46
40
|
|
|
@@ -82,7 +76,6 @@ Add these maintained extensions after installing Osdy Pi. They are optional; eac
|
|
|
82
76
|
| `pi-playwright` | Playwright browser-automation skills. |
|
|
83
77
|
| `pi-mcp-adapter` | MCP server and tool adapter for Pi. |
|
|
84
78
|
| `pi-subagents-j0k3r` | Markdown-defined subagents, delegation tools, history, and model profiles. |
|
|
85
|
-
| `@juicesharp/rpiv-todo` | A persistent live todo overlay for the agent. |
|
|
86
79
|
| `gentle-pi` | The Gentle senior-architect harness, with SDD/OpenSpec, subagents, TDD evidence, and skills. |
|
|
87
80
|
|
|
88
81
|
Install the ordinary npm packages once:
|
|
@@ -98,7 +91,6 @@ pi install npm:@open-pets/pi
|
|
|
98
91
|
pi install npm:pi-playwright
|
|
99
92
|
pi install npm:pi-mcp-adapter
|
|
100
93
|
pi install npm:pi-subagents-j0k3r
|
|
101
|
-
pi install npm:@juicesharp/rpiv-todo
|
|
102
94
|
```
|
|
103
95
|
|
|
104
96
|
`gentle-pi` is deliberately not included in that package-install block: clone and register it through the coexistence setup below, which applies Osdy-specific exclusions instead of adding a duplicate ordinary package entry.
|
|
@@ -128,11 +120,48 @@ To load a local `gentle-pi` checkout while keeping Osdy Pi's UI authoritative, r
|
|
|
128
120
|
osdy-pi gentle setup /absolute/path/to/gentle-pi
|
|
129
121
|
```
|
|
130
122
|
|
|
131
|
-
The command validates that the absolute source is a readable `gentle-pi` package with Gentle's todo and agents extensions before atomically updating `$PI_CODING_AGENT_DIR/settings.json` (or `~/.pi/agent/settings.json`). It registers the local package immediately before the first configured Osdy Pi package entry, so Gentle Shell initializes first, with exclusions
|
|
123
|
+
The command validates that the absolute source is a readable `gentle-pi` package with Gentle's todo and agents extensions before atomically updating `$PI_CODING_AGENT_DIR/settings.json` (or `~/.pi/agent/settings.json`). It registers the local package immediately before the first configured Osdy Pi package entry, so Gentle Shell initializes first, with exclusions for Gentle's `gentle-todo.ts`, `ask-user-question.ts`, and `gentle-agents.ts`, plus `themes: []`. Restart Pi or run `/reload` after setup.
|
|
132
124
|
|
|
133
125
|
Gentle Shell intentionally remains fully active underneath Osdy, including its footer and widgets. While Osdy is enabled, Osdy claims the footer and editor; disabling Osdy restores the editor Gentle Shell provided at session startup. Gentle's changes widget may coexist with Osdy's visual widgets.
|
|
134
126
|
|
|
135
|
-
The exclusions prevent Gentle's todo and agents extensions from competing with the suite.
|
|
127
|
+
The exclusions prevent Gentle's todo, questionnaire, and agents extensions from competing with the suite. Osdy's session-branch snapshots are authoritative for its `todo` tool and read-only `/todos` command; `pi-subagents-j0k3r` remains the subagent system and `@juicesharp/rpiv-ask-user-question` remains the structured-question plugin. This setup does **not** remove an independently installed `@juicesharp/rpiv-todo` package or edit its package entry. Remove that package yourself as described below to avoid duplicate `todo` and `/todos` registrations.
|
|
128
|
+
|
|
129
|
+
## First-party TODO included starting in 1.5.0
|
|
130
|
+
|
|
131
|
+
The model-facing `todo` tool supports `create`, `update`, `list`, `get`, `delete`, and `clear`. Tool results carry full task snapshots; Pi's current session branch is the TODO authority. Session switches and compaction replay the latest valid branch snapshot, rather than sharing one project-wide list. `/todos` opens a read-only, grouped modal (pending, in progress, completed) in interactive Pi; scroll with arrow or Page Up/Down keys and close with Esc or q. The refreshed modal adds a completed-task progress meter, theme-aware status groups and a scroll-position footer while keeping every task reachable. It is not an interactive Markdown editor. In non-terminal modes with UI support, it retains notification output. ODD's `odd/tasks/*.md` ledger and Engram are separate orchestration records: there is no automatic sync with these Pi TODOs.
|
|
132
|
+
|
|
133
|
+
In interactive Pi, the persistent widget appears above the editor while visible tasks exist. By default it shows up to five task rows (excluding heading, overflow hint, and spacer), summarizes overflow, and keeps completed rows visible with crossed-out subjects on subsequent turns; expand tool output to view all rows. `ctrl+shift+t` collapses or expands the widget.
|
|
134
|
+
|
|
135
|
+
### Try session TODO
|
|
136
|
+
|
|
137
|
+
Use a 1.5.0 checkout (or an installed 1.5.0 package once published), ask the agent to track a task with `todo`, then run `/todos`. The widget shows up to five tasks by default; the modal shows the entire grouped list with arrow/Page Up/Down scrolling. If your `maxWidgetLines` configuration overrides the default, set it to `7` to show five task rows.
|
|
138
|
+
|
|
139
|
+
**Verification to date (not complete):** Prior feature checks passed 187 extension tests, 60 script tests, typecheck, and lint. A disposable Pi TUI with seeded tasks showed the modal's progress meter, grouped rows, scroll footer, and close behavior. Authenticated RPC turns exercised all six `todo` actions, and an authenticated TUI smoke observed widget and `/todos` updates. An RPC lifecycle check observed an empty new session and the completed task on switching back. Compaction replay remains unverified because Pi returned “Nothing to compact (session too small).” Language switching in the authenticated TODO flow remains unverified (an isolated TUI with both extensions did switch English to Spanish). Visual strikethrough of completed rows in the authenticated TUI remains unverified. Launch under an installed Osdy account profile remains unverified. These checks do not establish full feature parity or validate a published npm artifact.
|
|
140
|
+
|
|
141
|
+
### TODO configuration and language
|
|
142
|
+
|
|
143
|
+
Create `$XDG_CONFIG_HOME/rpiv-todo/config.json` (with an absolute `XDG_CONFIG_HOME`); when absent, Osdy falls back to `~/.config/rpiv-todo/config.json`. Invalid JSON uses defaults. For example:
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"maxWidgetLines": 7,
|
|
148
|
+
"collapseKey": "ctrl+shift+t",
|
|
149
|
+
"guidance": {
|
|
150
|
+
"promptSnippet": "Track multi-step work",
|
|
151
|
+
"promptGuidelines": ["Update task status as work progresses"]
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`maxWidgetLines` must be a number >= 3 (otherwise 7); it is read on widget renders. `collapseKey` accepts a key combination or `"off"` to disable the shortcut; the shortcut binds at extension load, so restart Pi or run `/reload` after changing it. `guidance.promptSnippet` and `guidance.promptGuidelines` override the tool's model guidance at registration; reload after changing them. The optional `@juicesharp/rpiv-i18n` peer enables nine bundled locales (`de`, `en`, `es`, `fr`, `pt-BR`, `pt`, `ru`, `uk`, `zh`); without the SDK the UI uses English. To use `/languages`, also load its Pi extension (for example, `pi install npm:@juicesharp/rpiv-i18n` in your chosen profile); the peer dependency alone does not register that command. An isolated Pi TUI with both extensions loaded switched the TODO widget and `/todos` from English to Spanish.
|
|
157
|
+
|
|
158
|
+
**Already installed `@juicesharp/rpiv-todo`?** Both packages can register `todo` and `/todos`. Check `pi list` in the Pi profile you intend to use; to use Osdy's first-party implementation, explicitly remove the standalone package in that same profile:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
pi remove npm:@juicesharp/rpiv-todo
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
For a project-local installation, run `pi remove -l npm:@juicesharp/rpiv-todo` in that project instead. Restart Pi or run `/reload`, then check `/todos` shows session tasks, not a document panel. Osdy never uninstalls another package or silently changes package settings. If you need the standalone package, disable the conflicting extension via Pi package configuration instead.
|
|
136
165
|
|
|
137
166
|
## OpenAI account profiles
|
|
138
167
|
|
|
@@ -215,7 +244,7 @@ Before rename or removal, close this Pi process when it uses the target and **ma
|
|
|
215
244
|
| --- | --- |
|
|
216
245
|
| Themes | 14 built-in themes, including Osdy, Kanagawa, Dracula, Catppuccin, Matrix, and Lucent Orange palettes |
|
|
217
246
|
| Header and mascot | Independently selectable `osdy-theme`/`neon` headers and `current`/`bts` mascots |
|
|
218
|
-
| Messages |
|
|
247
|
+
| Messages | Native colored user cards without emojis and plain assistant Markdown with a separate assistant `🦝` row |
|
|
219
248
|
| Input | Responsive auto editor by default, with selectable simple Pi-native or extended framed modes |
|
|
220
249
|
| Status | Theme-aware Braille spinner and animated working label, responsive footer metrics, dynamic extension statuses, and Codex subscription quota with low-capacity emphasis |
|
|
221
250
|
| Git | Working-tree summary and a centered, filterable diff panel |
|
|
@@ -300,6 +329,7 @@ The enabled state, editor mode, working-tree visibility preference, header, and
|
|
|
300
329
|
| Main | `/osdy-pi` |
|
|
301
330
|
| Main | `/osdy-pi enable\|disable\|on\|off\|status` |
|
|
302
331
|
| Accounts | `/osdy-account` |
|
|
332
|
+
| Session TODO (starting in 1.5.0) | `/todos` (read-only) |
|
|
303
333
|
| Header | `/osdy-pi header osdy-theme\|neon\|status` |
|
|
304
334
|
| Mascot | `/osdy-pi mascot current\|bts\|status` |
|
|
305
335
|
| Editor | `/osdy-pi editor auto\|extended\|simple\|on\|off\|toggle\|status` |
|
|
@@ -325,9 +355,7 @@ Usage loads once when the session starts and refreshes when the modal opens or `
|
|
|
325
355
|
|
|
326
356
|
## Native message cards
|
|
327
357
|
|
|
328
|
-
Osdy Pi
|
|
329
|
-
|
|
330
|
-
User and assistant cards have separate background, text, and accent tokens. Assistant accents follow each theme's primary accent; user accents are white for a consistent visual distinction. Disabling Osdy Pi continues to restore the underlying Gentle Shell or Pi presentation normally.
|
|
358
|
+
Pi renders the colored user card and assistant Markdown natively. User messages have no emoji; Osdy Pi places a separate `🦝` transcript entry before each assistant response with visible text, not inside message content. Previously saved user `👤` entries remain in session history but render no row on replay. Selecting only the native message keeps its body clean; a wide selection that includes the emoji row copies the emoji too. Assistant markers persist with the session but do not enter model context. The installed Pi 0.87.1 does not use `assistantMessage*` theme palette keys, so these keys do not create a native assistant card. Osdy does not install a Markdown transformer or change streaming. The current user card colors remain unchanged. Try both roles in your selected theme with `npm run pi:dev`; terminal rendering and selection still need a live visual check. When disabled, Osdy does not add new markers.
|
|
331
359
|
|
|
332
360
|
## Editor and working indicator
|
|
333
361
|
|
|
@@ -335,7 +363,7 @@ The default `auto` editor mode preserves the responsive behavior: it uses the fr
|
|
|
335
363
|
|
|
336
364
|
In auto or extended mode at a non-small width, the framed editor shows the model and thinking level in its title and session usage in its footer. For an account-profile launch, the left title shows the active profile name instead of `Osdy-Pi`. When the native editor is effective (simple mode or any small terminal), the Osdy footer instead shows model, active profile when present, thinking, and usage rows before its path/branch and status rows. It uses the currently active Pi/Osdy theme palette; no separate editor theme selector exists. Usage covers input, output, cache read, cache write when present, cost, and context. If Pi supports autocomplete, the editor uses Pi's native autocomplete rendering while the completion UI is visible.
|
|
337
365
|
|
|
338
|
-
While work is active, Osdy Pi shows the original Braille spinner above the editor in the selected theme's accent color. A
|
|
366
|
+
While work is active, Osdy Pi shows the original Braille spinner above the editor in the selected theme's accent color. A two-color wave travels across the `Working...` and `Running ...` letters: the current letter uses the active theme's `accent`, the trailing letter uses `warning`, and the rest use `text`. Only the current `accent` letter tries the active theme's bold styling; this is a terminal-dependent visual trial, not a change to font size. The trailing `warning` letter stays plain, as does the accent spinner. The spinner and label update when the theme changes. Osdy Pi hides Pi's built-in working row while enabled to avoid a duplicate indicator.
|
|
339
367
|
|
|
340
368
|
## Working tree and diff
|
|
341
369
|
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type TUI,
|
|
7
7
|
} from "@earendil-works/pi-tui";
|
|
8
8
|
import type { CodexUsageSnapshot, CodexUsageWindow } from "./codex-usage.js";
|
|
9
|
+
import { doubleBorderBox, MODAL_OVERLAY_OPTIONS } from "./modal-frame.js";
|
|
9
10
|
import type { CodexUsageState, SimpleTheme } from "./types.js";
|
|
10
11
|
|
|
11
12
|
export type CodexUsageViewState = CodexUsageState;
|
|
@@ -18,13 +19,7 @@ export type CodexUsagePresentation = {
|
|
|
18
19
|
model?: string | undefined;
|
|
19
20
|
};
|
|
20
21
|
|
|
21
|
-
export const CODEX_USAGE_OVERLAY_OPTIONS =
|
|
22
|
-
anchor: "center",
|
|
23
|
-
width: 96,
|
|
24
|
-
minWidth: 48,
|
|
25
|
-
maxHeight: "92%",
|
|
26
|
-
margin: 1,
|
|
27
|
-
} as const;
|
|
22
|
+
export const CODEX_USAGE_OVERLAY_OPTIONS = MODAL_OVERLAY_OPTIONS;
|
|
28
23
|
|
|
29
24
|
const ESCAPE = String.fromCharCode(27);
|
|
30
25
|
const BELL = String.fromCharCode(7);
|
|
@@ -180,31 +175,6 @@ function borderBox(
|
|
|
180
175
|
];
|
|
181
176
|
}
|
|
182
177
|
|
|
183
|
-
function doubleBorderBox(
|
|
184
|
-
theme: SimpleTheme,
|
|
185
|
-
width: number,
|
|
186
|
-
title: string,
|
|
187
|
-
lines: string[],
|
|
188
|
-
): string[] {
|
|
189
|
-
const innerWidth = Math.max(1, width - 2);
|
|
190
|
-
const heading = truncateToWidth(` ${title} `, innerWidth, "...", true);
|
|
191
|
-
const leftWidth = Math.floor(
|
|
192
|
-
Math.max(0, innerWidth - visibleWidth(heading)) / 2,
|
|
193
|
-
);
|
|
194
|
-
const rightWidth = Math.max(0, innerWidth - visibleWidth(heading) - leftWidth);
|
|
195
|
-
const pad = (line: string): string => {
|
|
196
|
-
const text = truncateToWidth(line, innerWidth, "...", true);
|
|
197
|
-
return `${text}${" ".repeat(Math.max(0, innerWidth - visibleWidth(text)))}`;
|
|
198
|
-
};
|
|
199
|
-
return [
|
|
200
|
-
`${theme.fg("border", `╔${"═".repeat(leftWidth)}`)}${theme.fg("accent", heading)}${theme.fg("border", `${"═".repeat(rightWidth)}╗`)}`,
|
|
201
|
-
...lines.map(
|
|
202
|
-
(line) => `${theme.fg("border", "║")}${pad(line)}${theme.fg("border", "║")}`,
|
|
203
|
-
),
|
|
204
|
-
theme.fg("border", `╚${"═".repeat(innerWidth)}╝`),
|
|
205
|
-
];
|
|
206
|
-
}
|
|
207
|
-
|
|
208
178
|
function quotaTheme(name: "Session" | "Weekly"): "accent" | "mdLink" {
|
|
209
179
|
return name === "Session" ? "accent" : "mdLink";
|
|
210
180
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta.notes": "Auto-translated draft. Native-speaker review welcome via PR.",
|
|
3
|
+
"status.pending": "ausstehend",
|
|
4
|
+
"status.in_progress": "in Bearbeitung",
|
|
5
|
+
"status.completed": "erledigt",
|
|
6
|
+
"status.deleted": "gelöscht",
|
|
7
|
+
"overlay.heading": "Todos",
|
|
8
|
+
"overlay.more": "weitere",
|
|
9
|
+
"overlay.expandHint": "{key} zum Ausklappen",
|
|
10
|
+
"overlay.collapsed": "eingeklappt",
|
|
11
|
+
"command.no_todos": "Noch keine Todos. Bitte den Agenten, welche hinzuzufügen!",
|
|
12
|
+
"command.requires_interactive": "/todos erfordert den interaktiven Modus",
|
|
13
|
+
"command.section.pending": "── Ausstehend ──",
|
|
14
|
+
"command.section.in_progress": "── In Bearbeitung ──",
|
|
15
|
+
"command.section.completed": "── Erledigt ──"
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"status.pending": "pending",
|
|
3
|
+
"status.in_progress": "in progress",
|
|
4
|
+
"status.completed": "completed",
|
|
5
|
+
"status.deleted": "deleted",
|
|
6
|
+
"overlay.heading": "Todos",
|
|
7
|
+
"overlay.more": "more",
|
|
8
|
+
"overlay.expandHint": "{key} to expand",
|
|
9
|
+
"overlay.collapsed": "collapsed",
|
|
10
|
+
"command.no_todos": "No todos yet. Ask the agent to add some!",
|
|
11
|
+
"command.requires_interactive": "/todos requires interactive mode",
|
|
12
|
+
"command.section.pending": "── Pending ──",
|
|
13
|
+
"command.section.in_progress": "── In Progress ──",
|
|
14
|
+
"command.section.completed": "── Completed ──"
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta.notes": "Auto-translated draft. Native-speaker review welcome via PR.",
|
|
3
|
+
"status.pending": "pendientes",
|
|
4
|
+
"status.in_progress": "en curso",
|
|
5
|
+
"status.completed": "completadas",
|
|
6
|
+
"status.deleted": "eliminada",
|
|
7
|
+
"overlay.heading": "Tareas",
|
|
8
|
+
"overlay.more": "más",
|
|
9
|
+
"overlay.expandHint": "{key} para expandir",
|
|
10
|
+
"overlay.collapsed": "contraído",
|
|
11
|
+
"command.no_todos": "Sin tareas aún. ¡Pídele al agente que añada algunas!",
|
|
12
|
+
"command.requires_interactive": "/todos requiere modo interactivo",
|
|
13
|
+
"command.section.pending": "── Pendientes ──",
|
|
14
|
+
"command.section.in_progress": "── En curso ──",
|
|
15
|
+
"command.section.completed": "── Completadas ──"
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta.notes": "Auto-translated draft. Native-speaker review welcome via PR.",
|
|
3
|
+
"status.pending": "en attente",
|
|
4
|
+
"status.in_progress": "en cours",
|
|
5
|
+
"status.completed": "terminées",
|
|
6
|
+
"status.deleted": "supprimée",
|
|
7
|
+
"overlay.heading": "Tâches",
|
|
8
|
+
"overlay.more": "autres",
|
|
9
|
+
"overlay.expandHint": "{key} pour développer",
|
|
10
|
+
"overlay.collapsed": "réduit",
|
|
11
|
+
"command.no_todos": "Pas encore de tâches. Demandez à l'agent d'en ajouter !",
|
|
12
|
+
"command.requires_interactive": "/todos nécessite le mode interactif",
|
|
13
|
+
"command.section.pending": "── En attente ──",
|
|
14
|
+
"command.section.in_progress": "── En cours ──",
|
|
15
|
+
"command.section.completed": "── Terminées ──"
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta.notes": "Auto-translated draft. Native-speaker review welcome via PR.",
|
|
3
|
+
"status.pending": "pendentes",
|
|
4
|
+
"status.in_progress": "em andamento",
|
|
5
|
+
"status.completed": "concluídas",
|
|
6
|
+
"status.deleted": "excluída",
|
|
7
|
+
"overlay.heading": "Tarefas",
|
|
8
|
+
"overlay.more": "mais",
|
|
9
|
+
"overlay.expandHint": "{key} para expandir",
|
|
10
|
+
"overlay.collapsed": "recolhido",
|
|
11
|
+
"command.no_todos": "Nenhuma tarefa ainda. Peça ao agente para adicionar algumas!",
|
|
12
|
+
"command.requires_interactive": "/todos requer modo interativo",
|
|
13
|
+
"command.section.pending": "── Pendentes ──",
|
|
14
|
+
"command.section.in_progress": "── Em andamento ──",
|
|
15
|
+
"command.section.completed": "── Concluídas ──"
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta.notes": "Auto-translated draft. Native-speaker review welcome via PR.",
|
|
3
|
+
"status.pending": "pendentes",
|
|
4
|
+
"status.in_progress": "em curso",
|
|
5
|
+
"status.completed": "concluídas",
|
|
6
|
+
"status.deleted": "eliminada",
|
|
7
|
+
"overlay.heading": "Tarefas",
|
|
8
|
+
"overlay.more": "mais",
|
|
9
|
+
"overlay.expandHint": "{key} para expandir",
|
|
10
|
+
"overlay.collapsed": "recolhido",
|
|
11
|
+
"command.no_todos": "Sem tarefas ainda. Peça ao agente para adicionar algumas!",
|
|
12
|
+
"command.requires_interactive": "/todos requer modo interativo",
|
|
13
|
+
"command.section.pending": "── Pendentes ──",
|
|
14
|
+
"command.section.in_progress": "── Em curso ──",
|
|
15
|
+
"command.section.completed": "── Concluídas ──"
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta.notes": "Автоперевод. Приветствуется проверка носителем языка через PR.",
|
|
3
|
+
"status.pending": "ожидание",
|
|
4
|
+
"status.in_progress": "в работе",
|
|
5
|
+
"status.completed": "выполнено",
|
|
6
|
+
"status.deleted": "удалена",
|
|
7
|
+
"overlay.heading": "Задачи",
|
|
8
|
+
"overlay.more": "ещё",
|
|
9
|
+
"overlay.expandHint": "{key} — развернуть",
|
|
10
|
+
"overlay.collapsed": "свёрнуто",
|
|
11
|
+
"command.no_todos": "Задач пока нет. Попросите агента добавить!",
|
|
12
|
+
"command.requires_interactive": "/todos требует интерактивного режима",
|
|
13
|
+
"command.section.pending": "── Ожидание ──",
|
|
14
|
+
"command.section.in_progress": "── В работе ──",
|
|
15
|
+
"command.section.completed": "── Выполнено ──"
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta.notes": "Автопереклад. Привітна перевірка носієм мови через PR.",
|
|
3
|
+
"status.pending": "очікування",
|
|
4
|
+
"status.in_progress": "у роботі",
|
|
5
|
+
"status.completed": "виконано",
|
|
6
|
+
"status.deleted": "видалено",
|
|
7
|
+
"overlay.heading": "Завдання",
|
|
8
|
+
"overlay.more": "ще",
|
|
9
|
+
"overlay.expandHint": "{key} — розгорнути",
|
|
10
|
+
"overlay.collapsed": "згорнуто",
|
|
11
|
+
"command.no_todos": "Завдань поки немає. Попросіть агента додати!",
|
|
12
|
+
"command.requires_interactive": "/todos потребує інтерактивного режиму",
|
|
13
|
+
"command.section.pending": "── Очікування ──",
|
|
14
|
+
"command.section.in_progress": "── У роботі ──",
|
|
15
|
+
"command.section.completed": "── Виконано ──"
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta.notes": "中文翻译。欢迎中文母语者通过 PR 审校。",
|
|
3
|
+
"status.pending": "待处理",
|
|
4
|
+
"status.in_progress": "进行中",
|
|
5
|
+
"status.completed": "已完成",
|
|
6
|
+
"status.deleted": "已删除",
|
|
7
|
+
"overlay.heading": "任务清单",
|
|
8
|
+
"overlay.more": "更多",
|
|
9
|
+
"overlay.expandHint": "按 {key} 展开",
|
|
10
|
+
"overlay.collapsed": "已折叠",
|
|
11
|
+
"command.no_todos": "暂无任务。可以让 agent 添加一些!",
|
|
12
|
+
"command.requires_interactive": "/todos 需要交互模式",
|
|
13
|
+
"command.section.pending": "── 待处理 ──",
|
|
14
|
+
"command.section.in_progress": "── 进行中 ──",
|
|
15
|
+
"command.section.completed": "── 已完成 ──"
|
|
16
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { registerHooks } from "node:module";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import ts from "typescript";
|
|
7
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
|
|
9
|
+
registerHooks({
|
|
10
|
+
resolve(specifier, context, nextResolve) {
|
|
11
|
+
if (specifier.startsWith(".") && specifier.endsWith(".js") && context.parentURL?.endsWith(".ts")) {
|
|
12
|
+
const url = new URL(`${specifier.slice(0, -3)}.ts`, context.parentURL);
|
|
13
|
+
if (existsSync(fileURLToPath(url))) return { shortCircuit: true, url: url.href };
|
|
14
|
+
}
|
|
15
|
+
return nextResolve(specifier, context);
|
|
16
|
+
},
|
|
17
|
+
load(url, context, nextLoad) {
|
|
18
|
+
if (url.endsWith(".ts")) return { format: "module", shortCircuit: true, source: ts.transpileModule(readFileSync(fileURLToPath(url), "utf8"), { compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 } }).outputText };
|
|
19
|
+
return nextLoad(url, context);
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
const { registerMessageRoleMarkers } = await import("./message-role-markers.js");
|
|
23
|
+
|
|
24
|
+
type Handler = (event: { message?: { role: string } }, ctx: ExtensionContext) => void;
|
|
25
|
+
type TestRenderer = (entry: { data?: { role?: unknown } }, options: { expanded: boolean }, theme: { fg: (color: string, text: string) => string }) => { render(width: number): string[] } | undefined;
|
|
26
|
+
function harness(mode = "tui", enabled = true) {
|
|
27
|
+
const handlers = new Map<string, Handler>();
|
|
28
|
+
const entries: Array<{ type: "custom"; customType: string; data: { role: string } }> = [];
|
|
29
|
+
const timeline: string[] = [];
|
|
30
|
+
let renderer: TestRenderer | undefined;
|
|
31
|
+
let customType = "";
|
|
32
|
+
const ctx = { mode, hasUI: true, sessionManager: { getBranch: () => entries } } as unknown as ExtensionContext;
|
|
33
|
+
const pi = {
|
|
34
|
+
on: (name: string, handler: Handler) => { handlers.set(name, handler); },
|
|
35
|
+
appendEntry: (type: string, data: { role: string }) => { entries.push({ type: "custom", customType: type, data }); timeline.push(`marker:${data.role}`); },
|
|
36
|
+
registerEntryRenderer: (type: string, render: unknown) => { customType = type; renderer = render as TestRenderer; },
|
|
37
|
+
} as unknown as ExtensionAPI;
|
|
38
|
+
registerMessageRoleMarkers(pi, () => enabled);
|
|
39
|
+
const emit = (name: string, message?: { role: string }) => { handlers.get(name)?.(message ? { message } : {}, ctx); if (name === "message_start" && message) timeline.push(`native:${message.role}`); };
|
|
40
|
+
return { emit, entries, timeline, ctx, get renderer() { return renderer; }, get customType() { return customType; }, setEnabled: (value: boolean) => { enabled = value; } };
|
|
41
|
+
}
|
|
42
|
+
const message = (role: string, timestamp: number, content: unknown[] = []) => ({ role, timestamp, content });
|
|
43
|
+
|
|
44
|
+
void test("user starts preserve the native card without appending an emoji or changing content", () => {
|
|
45
|
+
const h = harness();
|
|
46
|
+
const user = message("user", 1, [{ type: "text", text: "original" }]);
|
|
47
|
+
const original = structuredClone(user);
|
|
48
|
+
h.emit("message_start", user);
|
|
49
|
+
h.emit("message_start", user);
|
|
50
|
+
h.emit("message_end", user);
|
|
51
|
+
assert.deepEqual(h.timeline, ["native:user", "native:user"]);
|
|
52
|
+
assert.deepEqual(user, original);
|
|
53
|
+
assert.deepEqual(h.entries, []);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
void test("assistant waits for visible text, then marks once across updates and end", () => {
|
|
57
|
+
const h = harness();
|
|
58
|
+
const assistant = message("assistant", 2);
|
|
59
|
+
h.emit("message_start", assistant);
|
|
60
|
+
h.emit("message_update", message("assistant", 2, [{ type: "thinking", thinking: "secret" }, { type: "toolCall", id: "t" }]));
|
|
61
|
+
assert.equal(h.entries.length, 0);
|
|
62
|
+
const text = message("assistant", 2, [{ type: "text", text: "answer" }]);
|
|
63
|
+
const original = structuredClone(text);
|
|
64
|
+
h.emit("message_update", text);
|
|
65
|
+
h.emit("message_update", text);
|
|
66
|
+
h.emit("message_end", text);
|
|
67
|
+
assert.deepEqual(h.timeline, ["native:assistant", "marker:assistant"]);
|
|
68
|
+
assert.equal(h.entries.length, 1);
|
|
69
|
+
assert.deepEqual(text, original);
|
|
70
|
+
h.emit("message_start", message("assistant", 3));
|
|
71
|
+
h.emit("message_end", message("assistant", 3, [{ type: "text", text: "final" }]));
|
|
72
|
+
assert.equal(h.entries.length, 2);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
void test("empty, thinking-only, tool-only and tool results have no assistant marker", () => {
|
|
76
|
+
const h = harness();
|
|
77
|
+
for (const [index, content] of [[], [{ type: "thinking", thinking: "x" }], [{ type: "toolCall", id: "t" }], [{ type: "text", text: " " }]].entries()) {
|
|
78
|
+
h.emit("message_start", message("assistant", index));
|
|
79
|
+
h.emit("message_end", message("assistant", index, content));
|
|
80
|
+
}
|
|
81
|
+
h.emit("message_start", message("toolResult", 5, [{ type: "text", text: "output" }]));
|
|
82
|
+
assert.deepEqual(h.entries, []);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
void test("disabled and noninteractive modes do not persist markers", () => {
|
|
86
|
+
const disabled = harness("tui", false);
|
|
87
|
+
disabled.emit("message_start", message("user", 1));
|
|
88
|
+
disabled.emit("message_end", message("assistant", 2, [{ type: "text", text: "a" }]));
|
|
89
|
+
assert.equal(disabled.entries.length, 0);
|
|
90
|
+
const json = harness("json");
|
|
91
|
+
json.emit("message_start", message("user", 1));
|
|
92
|
+
json.emit("message_end", message("assistant", 2, [{ type: "text", text: "a" }]));
|
|
93
|
+
assert.equal(json.entries.length, 0);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
void test("same-timestamp assistant answers each get one marker without user markers", () => {
|
|
97
|
+
const h = harness();
|
|
98
|
+
const firstUser = message("user", 1);
|
|
99
|
+
const secondUser = message("user", 1);
|
|
100
|
+
h.emit("message_start", firstUser);
|
|
101
|
+
h.emit("message_start", firstUser);
|
|
102
|
+
h.emit("message_start", secondUser);
|
|
103
|
+
for (let i = 0; i < 2; i++) {
|
|
104
|
+
h.emit("message_start", message("assistant", 2));
|
|
105
|
+
h.emit("message_update", message("assistant", 2, [{ type: "text", text: `answer ${i}` }]));
|
|
106
|
+
h.emit("message_end", message("assistant", 2, [{ type: "text", text: `answer ${i}` }]));
|
|
107
|
+
}
|
|
108
|
+
assert.deepEqual(h.entries.map(({ data }) => data), [{ role: "assistant" }, { role: "assistant" }]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
void test("user start and session change clear pending assistant state", () => {
|
|
112
|
+
const h = harness();
|
|
113
|
+
h.emit("message_start", message("assistant", 1));
|
|
114
|
+
h.emit("message_start", message("user", 2));
|
|
115
|
+
h.emit("message_end", message("assistant", 1, [{ type: "text", text: "orphan" }]));
|
|
116
|
+
assert.equal(h.entries.length, 0);
|
|
117
|
+
h.emit("message_start", message("assistant", 3));
|
|
118
|
+
h.emit("session_start");
|
|
119
|
+
h.emit("message_end", message("assistant", 3, [{ type: "text", text: "orphan" }]));
|
|
120
|
+
assert.equal(h.entries.length, 0);
|
|
121
|
+
h.emit("message_start", message("assistant", 4));
|
|
122
|
+
h.emit("message_end", message("assistant", 4, [{ type: "text", text: "new" }]));
|
|
123
|
+
assert.deepEqual(h.entries.map(({ data }) => data.role), ["assistant"]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
void test("malformed assistant content fails closed without throwing", () => {
|
|
127
|
+
const h = harness();
|
|
128
|
+
h.emit("message_start", message("assistant", 1));
|
|
129
|
+
for (const content of [null, {}, [null], [{ type: "text", text: {} }]])
|
|
130
|
+
h.emit("message_update", { role: "assistant", timestamp: 1, content } as never);
|
|
131
|
+
assert.equal(h.entries.length, 0);
|
|
132
|
+
h.emit("message_end", message("assistant", 1, [{ type: "text", text: "valid" }]));
|
|
133
|
+
assert.deepEqual(h.entries.map(({ data }) => data), [{ role: "assistant" }]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
void test("legacy user entries render no row, while assistant glyph stays copy-isolated", () => {
|
|
137
|
+
const h = harness();
|
|
138
|
+
h.emit("message_start", message("assistant", 2));
|
|
139
|
+
h.emit("message_update", message("assistant", 2, [{ type: "text", text: "secret body" }]));
|
|
140
|
+
const theme = { fg: (_color: string, text: string) => text };
|
|
141
|
+
const legacy = { type: "custom", customType: h.customType, data: { role: "user" } };
|
|
142
|
+
// Pi's addCustomEntryToChat skips the whole row when this renderer returns undefined.
|
|
143
|
+
assert.equal(legacy.customType, h.entries[0]?.customType);
|
|
144
|
+
assert.equal(h.renderer?.(legacy, { expanded: false }, theme), undefined);
|
|
145
|
+
const assistantLines = h.renderer?.(h.entries[0]!, { expanded: false }, theme)?.render(40) ?? [];
|
|
146
|
+
assert.match(assistantLines.join(""), /🦝/);
|
|
147
|
+
assert.doesNotMatch(assistantLines.join(""), /secret body/);
|
|
148
|
+
assert.equal(h.entries[0]?.customType, h.customType);
|
|
149
|
+
assert.equal(h.renderer?.({ data: { role: "invalid" } }, { expanded: false }, theme), undefined);
|
|
150
|
+
assert.equal(h.renderer?.({ data: {} }, { expanded: false }, theme), undefined);
|
|
151
|
+
assert.equal(h.renderer?.({ data: { role: "assistant" } }, { expanded: false }, theme)?.render(1).join(""), "");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
void test("renderer returns an empty line for invalid or narrow widths", () => {
|
|
155
|
+
const h = harness();
|
|
156
|
+
const renderer = h.renderer?.({ data: { role: "assistant" } }, { expanded: false }, { fg: (_color: string, text: string) => text });
|
|
157
|
+
assert.ok(renderer);
|
|
158
|
+
for (const width of [0, 1, -1, NaN, Infinity, -Infinity, 2.5])
|
|
159
|
+
assert.deepEqual(renderer.render(width), [""], `width ${width}`);
|
|
160
|
+
assert.match(renderer.render(2).join(""), /🦝/);
|
|
161
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
const ENTRY_TYPE = "osdy-pi-role-marker";
|
|
5
|
+
type Marker = { role: "assistant" };
|
|
6
|
+
|
|
7
|
+
export function registerMessageRoleMarkers(
|
|
8
|
+
pi: Pick<ExtensionAPI, "on" | "appendEntry" | "registerEntryRenderer">,
|
|
9
|
+
isEnabled: () => boolean,
|
|
10
|
+
): void {
|
|
11
|
+
let pendingAssistant = false;
|
|
12
|
+
const eligible = (ctx: ExtensionContext): boolean => isEnabled() && ctx.mode === "tui" && ctx.hasUI;
|
|
13
|
+
const visible = (content: unknown): boolean =>
|
|
14
|
+
Array.isArray(content) && (content as unknown[]).some((part) => {
|
|
15
|
+
if (part === null || typeof part !== "object") return false;
|
|
16
|
+
const block = part as { type?: unknown; text?: unknown };
|
|
17
|
+
return block.type === "text" && typeof block.text === "string" && Boolean(block.text.trim());
|
|
18
|
+
});
|
|
19
|
+
const reset = (): void => { pendingAssistant = false; };
|
|
20
|
+
|
|
21
|
+
pi.registerEntryRenderer<Marker>(ENTRY_TYPE, (entry, _options, theme) => {
|
|
22
|
+
const role = entry.data?.role;
|
|
23
|
+
if (role !== "assistant") return undefined;
|
|
24
|
+
const text = new Text(theme.fg("muted", "🦝"), 0, 0);
|
|
25
|
+
return {
|
|
26
|
+
render: (width: number) => !Number.isInteger(width) || width < 2 ? [""] : text.render(width),
|
|
27
|
+
invalidate: () => text.invalidate(),
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
pi.on("message_start", (event, ctx) => {
|
|
31
|
+
if (event.message.role === "user") {
|
|
32
|
+
pendingAssistant = false;
|
|
33
|
+
} else if (event.message.role === "assistant") {
|
|
34
|
+
pendingAssistant = eligible(ctx);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
pi.on("message_update", (event, ctx) => {
|
|
38
|
+
if (event.message.role === "assistant" && pendingAssistant && eligible(ctx) && visible(event.message.content)) {
|
|
39
|
+
pi.appendEntry<Marker>(ENTRY_TYPE, { role: "assistant" });
|
|
40
|
+
pendingAssistant = false;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
pi.on("message_end", (event, ctx) => {
|
|
44
|
+
if (event.message.role !== "assistant") return;
|
|
45
|
+
if (pendingAssistant && eligible(ctx) && visible(event.message.content))
|
|
46
|
+
pi.appendEntry<Marker>(ENTRY_TYPE, { role: "assistant" });
|
|
47
|
+
pendingAssistant = false;
|
|
48
|
+
});
|
|
49
|
+
pi.on("session_start", reset);
|
|
50
|
+
pi.on("session_tree", reset);
|
|
51
|
+
pi.on("session_shutdown", reset);
|
|
52
|
+
}
|