pi-note 0.22.0 → 0.23.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/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # pi-note
2
2
 
3
+ [npm](https://www.npmjs.com/package/pi-note) · [Pi package directory](https://pi.dev/packages/pi-note)
4
+
3
5
  Persistent [pinote](https://github.com/kvidzibo/pinote) tasks and Markdown handoffs
4
6
  in Pi: selected-task footer status, `/pi-note`, and agent current-task read/update,
5
7
  add, and tag-listing tools.
@@ -12,10 +14,8 @@ explicit initial-version limits; it is not activated by installing this extensio
12
14
  ## Install
13
15
 
14
16
  Requires Pi 0.99.1+, Node.js 22.19+ and **pinote 0.3.0+** (`note` on PATH). Add and tag listing need **pinote 0.4.0+**.
15
- From this checkout, install the extension with `pi install ./pi`, then run
16
- `/reload` in interactive Pi. The planned npm package name is `pi-note`; after its
17
- first release use `pi install npm:pi-note` instead. It is not published yet. npm
18
- installation does not run Python installers.
17
+ Install with `pi install npm:pi-note`, or `pi install ./pi` from this checkout,
18
+ then run `/reload` in interactive Pi. npm installation does not run Python installers.
19
19
 
20
20
  ### CLI setup and upgrades
21
21
 
@@ -390,13 +390,13 @@ data. Run visual checks under Xvfb/private D-Bus; never use personal task data.
390
390
  OIDC trusted publishing and provenance. Existing versions are skipped; registry
391
391
  failures fail the workflow. No npm token is stored in GitHub.
392
392
 
393
- One-time maintainer setup (not performed by installing this package):
393
+ Only a package's first publication needs this manual bootstrap (not part of
394
+ installation). `pi-note` is already published; use automated releases above.
394
395
 
395
396
  1. From `pi/`, run `npm login`, then `npm publish --ignore-scripts --access public`
396
397
  to bootstrap `pi-note` after checking name availability.
397
398
  2. In npm's package settings configure the GitHub trusted publisher: owner
398
399
  `kvidzibo`, repository `pinote`, workflow `publish.yml`, no environment.
399
- 3. Add verified npm and Pi package-directory links here after publication.
400
400
 
401
401
  The CLI source pin in `setup.ts` must point to a verified immutable commit with a
402
402
  compatible Python package. Update it and the bundled-version setup text together
@@ -27,7 +27,7 @@ const Action = z.object({ method: z.literal('native-ui/action'), params: z.objec
27
27
  uri: z.literal(URI), revision: z.string().min(1).max(256), action: z.string().min(1).max(256),
28
28
  value: z.union([z.string().max(20_000), z.number().finite(), z.boolean(), z.null()]).optional(), _meta: z.record(z.unknown()).optional(),
29
29
  }) });
30
- const server = new Server({ name: 'pinote', version: '0.22.0' }, { capabilities: { tools: {}, resources: {}, experimental: { 'native-ui-v1': {} } } });
30
+ const server = new Server({ name: 'pinote', version: '0.23.0' }, { capabilities: { tools: {}, resources: {}, experimental: { 'native-ui-v1': {} } } });
31
31
  // Serialize complete read/check/write cycles even when a host sends parallel calls.
32
32
  let queue = Promise.resolve();
33
33
  function serial(run) {
@@ -137,7 +137,13 @@ function staleAction() {
137
137
  function secondaryActions(meta) {
138
138
  return z.object({ footerSecondaryActions: z.literal(true) }).safeParse(meta?.['native-ui/features']).success;
139
139
  }
140
- function view(data, scoped = false, secondary = false) {
140
+ function footerFeatures(meta) {
141
+ return {
142
+ footerStyles: z.object({ footerStyles: z.literal(true) }).safeParse(meta?.['native-ui/features']).success,
143
+ footerDoubleActions: z.object({ footerDoubleActions: z.literal(true) }).safeParse(meta?.['native-ui/features']).success,
144
+ };
145
+ }
146
+ function view(data, scoped = false, secondary = false, features = {}) {
141
147
  const { tasks, task, config, revision } = data;
142
148
  const state = Context.parse({ conversationId: 'outgoing-state', state: { ...data.state, selectedId: task?.id ?? null } }).state;
143
149
  const controls = [];
@@ -196,11 +202,11 @@ function view(data, scoped = false, secondary = false) {
196
202
  const footer = [{ text: `${icons.note} `, width: 3 }];
197
203
  if (task) {
198
204
  footer.push({ text: `${icons.check} `, action: 'done', width: 3 });
199
- footer.push({ text: `${task.tag ? `[${task.tag}] ` : ''}${task.text.split('\n')[0].slice(0, 2000)}`, action: 'preview', width: config.config.titleWidth });
205
+ footer.push({ text: `${task.tag ? `[${task.tag}] ` : ''}${task.text.split('\n')[0].slice(0, 2000)}`, action: 'preview', ...(features.footerDoubleActions ? { doubleAction: 'done' } : {}), width: config.config.titleWidth });
200
206
  }
201
207
  else if (state.proposal) {
202
208
  footer.push({ text: `${icons.add} `, action: 'proposal:accept', ...(secondary ? { secondaryAction: 'proposal:dismiss' } : {}), width: 3 });
203
- footer.push({ text: `${state.proposal.tag ? `[${state.proposal.tag}] ` : ''}${state.proposal.text.split('\n')[0].slice(0, 2000)}`, width: config.config.titleWidth });
209
+ footer.push({ text: `${state.proposal.tag ? `[${state.proposal.tag}] ` : ''}${state.proposal.text.split('\n')[0].slice(0, 2000)}`, ...(features.footerDoubleActions ? { doubleAction: 'proposal:accept' } : {}), ...(features.footerStyles ? { style: { italic: true, color: 'accent' } } : {}), width: config.config.titleWidth });
204
210
  }
205
211
  else
206
212
  footer.push({ text: 'Pinote · no task', width: config.config.titleWidth });
@@ -256,7 +262,7 @@ server.setRequestHandler(ReadResourceRequestSchema, (request, extra) => serial(a
256
262
  if (request.params.uri !== URI)
257
263
  throw new Error('Unknown resource');
258
264
  const data = await snapshot(context(request.params._meta).state, extra.signal);
259
- return { contents: [{ uri: URI, mimeType: MIME, text: JSON.stringify(view(data, scopedActions(request.params._meta), secondaryActions(request.params._meta))) }] };
265
+ return { contents: [{ uri: URI, mimeType: MIME, text: JSON.stringify(view(data, scopedActions(request.params._meta), secondaryActions(request.params._meta), footerFeatures(request.params._meta))) }] };
260
266
  }));
261
267
  server.setRequestHandler(Action, (request, extra) => serial(async () => {
262
268
  const state = context(request.params._meta).state;
@@ -354,7 +360,7 @@ server.setRequestHandler(Action, (request, extra) => serial(async () => {
354
360
  extra.signal.throwIfAborted();
355
361
  saveFooterConfig(cfg, before.config.raw, handoff, newSession, policy);
356
362
  }
357
- const result = { view: view(await snapshot(next, extra.signal), scoped, secondaryActions(request.params._meta)), ...(draft ? { draft } : {}), ...(preview ? { preview } : {}) };
363
+ const result = { view: view(await snapshot(next, extra.signal), scoped, secondaryActions(request.params._meta), footerFeatures(request.params._meta)), ...(draft ? { draft } : {}), ...(preview ? { preview } : {}) };
358
364
  await server.notification({ method: 'notifications/native-ui/changed', params: { uri: URI } });
359
365
  return result;
360
366
  }));
package/mcp/README.md CHANGED
@@ -18,11 +18,12 @@ npm --prefix pi ci --ignore-scripts
18
18
  npm --prefix pi run build:mcp
19
19
  ```
20
20
 
21
- Add a service to the gateway config (use absolute paths):
21
+ Merge the service into your gateway's existing `mcpServers` map, preserving other
22
+ services (use absolute paths). Do not mix `mcpServers` with the legacy `servers` key:
22
23
 
23
24
  ```json
24
25
  {
25
- "servers": {
26
+ "mcpServers": {
26
27
  "pinote": {
27
28
  "command": "node",
28
29
  "args": ["/absolute/path/to/pinote/pi/dist-mcp/mcp/server.js"],
@@ -54,13 +55,18 @@ normal personal data. No desktop notifications are sent.
54
55
  edits are rejected and unrelated settings are preserved. Invalid configuration
55
56
  fails closed; repair it manually rather than replacing it with defaults.
56
57
  - **Footer:** selected task plus configured fields; safe HTTP(S) links are clickable.
57
- The title previews the task; **✔** completes it and clears the selection without
58
- resetting the conversation or editor. Suggestions show only **✚**: left-click to
58
+ Single-click the active title to preview; double-click it to complete. **✔** also
59
+ completes and clears the selection without resetting the conversation or editor.
60
+ Suggested titles use italic accent text; double-click to accept/start, then the
61
+ title returns to normal text. Acceptance cannot reuse the same click capability
62
+ to complete. Styling/double-click require gateway 0.28.0+ and double-click needs
63
+ its updated Kitty mouse mapping (Ctrl+Shift+double-click in fullscreen).
64
+ Older hosts retain plain titles and icon actions. Suggestions show **✚**: left-click to
59
65
  create/start/select, right-click to dismiss. Right-click requires gateway 0.22.0+
60
66
  and its Kitty mouse mapping; **Dismiss suggestion** in the app menu remains the
61
67
  keyboard/older-host fallback. These use the gateway's generic footer actions, not a Pinote
62
- extension or private handler. Install the gateway's Kitty protocol handler as
63
- and right-click mouse mapping described in its native UI documentation. Existing
68
+ extension or private handler. Install the gateway's Kitty protocol handler
69
+ and right-click mouse mapping as described in its native UI documentation. Existing
64
70
  installations need to add the mapping and reload Kitty; package updates do not
65
71
  edit terminal configuration. App menus remain the keyboard fallback.
66
72
  - **Agent:** `propose`, `get_current`, `update_current`, `fields`, and `tags` are
package/mcp/server.ts CHANGED
@@ -32,7 +32,7 @@ const Action = z.object({ method: z.literal('native-ui/action'), params: z.objec
32
32
  uri: z.literal(URI), revision: z.string().min(1).max(256), action: z.string().min(1).max(256),
33
33
  value: z.union([z.string().max(20_000), z.number().finite(), z.boolean(), z.null()]).optional(), _meta: z.record(z.unknown()).optional(),
34
34
  }) });
35
- const server = new Server({ name: 'pinote', version: '0.22.0' }, { capabilities: { tools: {}, resources: {}, experimental: { 'native-ui-v1': {} } } });
35
+ const server = new Server({ name: 'pinote', version: '0.23.0' }, { capabilities: { tools: {}, resources: {}, experimental: { 'native-ui-v1': {} } } });
36
36
  // Serialize complete read/check/write cycles even when a host sends parallel calls.
37
37
  let queue = Promise.resolve();
38
38
  function serial<T>(run: () => Promise<T>): Promise<T> {
@@ -118,7 +118,13 @@ function staleAction(): never {
118
118
  function secondaryActions(meta: Record<string, unknown> | undefined): boolean {
119
119
  return z.object({ footerSecondaryActions: z.literal(true) }).safeParse(meta?.['native-ui/features']).success;
120
120
  }
121
- function view(data: Snapshot, scoped = false, secondary = false) {
121
+ function footerFeatures(meta: Record<string, unknown> | undefined) {
122
+ return {
123
+ footerStyles: z.object({ footerStyles: z.literal(true) }).safeParse(meta?.['native-ui/features']).success,
124
+ footerDoubleActions: z.object({ footerDoubleActions: z.literal(true) }).safeParse(meta?.['native-ui/features']).success,
125
+ };
126
+ }
127
+ function view(data: Snapshot, scoped = false, secondary = false, features: { footerStyles?: boolean; footerDoubleActions?: boolean } = {}) {
122
128
  const { tasks, task, config, revision } = data;
123
129
  const state = Context.parse({ conversationId: 'outgoing-state', state: { ...data.state, selectedId: task?.id ?? null } }).state;
124
130
  const controls: Control[] = [];
@@ -164,13 +170,13 @@ function view(data: Snapshot, scoped = false, secondary = false) {
164
170
  for (const candidate of matches.slice(0, 200)) action(`select:${candidate.id}`, `${candidate.tag ? `[${candidate.tag}] ` : ''}${candidate.text.split('\n')[0]}`, { description: `#${candidate.id} · ${candidate.state}`, confirm: 'Select and start this task for this conversation?' });
165
171
  if (matches.length > 200) body += '\nShowing the first 200 matches; use Filter tasks to narrow the list.';
166
172
  }
167
- const footer: { text: string; url?: string; action?: string; secondaryAction?: string; width?: number }[] = [{ text: `${icons.note} `, width: 3 }];
173
+ const footer: { text: string; url?: string; action?: string; secondaryAction?: string; doubleAction?: string; style?: { italic: boolean; color: 'accent' }; width?: number }[] = [{ text: `${icons.note} `, width: 3 }];
168
174
  if (task) {
169
175
  footer.push({ text: `${icons.check} `, action: 'done', width: 3 });
170
- footer.push({ text: `${task.tag ? `[${task.tag}] ` : ''}${task.text.split('\n')[0].slice(0, 2000)}`, action: 'preview', width: config.config.titleWidth });
176
+ footer.push({ text: `${task.tag ? `[${task.tag}] ` : ''}${task.text.split('\n')[0].slice(0, 2000)}`, action: 'preview', ...(features.footerDoubleActions ? { doubleAction: 'done' } : {}), width: config.config.titleWidth });
171
177
  } else if (state.proposal) {
172
178
  footer.push({ text: `${icons.add} `, action: 'proposal:accept', ...(secondary ? { secondaryAction: 'proposal:dismiss' } : {}), width: 3 });
173
- footer.push({ text: `${state.proposal.tag ? `[${state.proposal.tag}] ` : ''}${state.proposal.text.split('\n')[0].slice(0, 2000)}`, width: config.config.titleWidth });
179
+ footer.push({ text: `${state.proposal.tag ? `[${state.proposal.tag}] ` : ''}${state.proposal.text.split('\n')[0].slice(0, 2000)}`, ...(features.footerDoubleActions ? { doubleAction: 'proposal:accept' } : {}), ...(features.footerStyles ? { style: { italic: true, color: 'accent' } as const } : {}), width: config.config.titleWidth });
174
180
  } else footer.push({ text: 'Pinote · no task', width: config.config.titleWidth });
175
181
  const titleParts = footer.length;
176
182
  if (task) {
@@ -211,7 +217,7 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [
211
217
  server.setRequestHandler(ReadResourceRequestSchema, (request, extra) => serial(async () => {
212
218
  if (request.params.uri !== URI) throw new Error('Unknown resource');
213
219
  const data = await snapshot(context(request.params._meta).state, extra.signal);
214
- return { contents: [{ uri: URI, mimeType: MIME, text: JSON.stringify(view(data, scopedActions(request.params._meta), secondaryActions(request.params._meta))) }] };
220
+ return { contents: [{ uri: URI, mimeType: MIME, text: JSON.stringify(view(data, scopedActions(request.params._meta), secondaryActions(request.params._meta), footerFeatures(request.params._meta))) }] };
215
221
  }));
216
222
  server.setRequestHandler(Action, (request, extra) => serial(async () => {
217
223
  const state = context(request.params._meta).state;
@@ -275,7 +281,7 @@ server.setRequestHandler(Action, (request, extra) => serial(async () => {
275
281
  extra.signal.throwIfAborted();
276
282
  saveFooterConfig(cfg, before.config.raw, handoff, newSession, policy);
277
283
  }
278
- const result = { view: view(await snapshot(next, extra.signal), scoped, secondaryActions(request.params._meta)), ...(draft ? { draft } : {}), ...(preview ? { preview } : {}) };
284
+ const result = { view: view(await snapshot(next, extra.signal), scoped, secondaryActions(request.params._meta), footerFeatures(request.params._meta)), ...(draft ? { draft } : {}), ...(preview ? { preview } : {}) };
279
285
  await server.notification({ method: 'notifications/native-ui/changed', params: { uri: URI } });
280
286
  return result;
281
287
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-note",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Persistent pinote tasks, Markdown handoffs, and task status for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",