pi-note 0.21.0 → 0.22.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/dist-mcp/mcp/server.js +11 -8
- package/mcp/README.md +7 -3
- package/mcp/server.ts +12 -9
- package/package.json +1 -1
package/dist-mcp/mcp/server.js
CHANGED
|
@@ -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.
|
|
30
|
+
const server = new Server({ name: 'pinote', version: '0.22.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) {
|
|
@@ -134,7 +134,10 @@ function actionRevision(data, control) {
|
|
|
134
134
|
function staleAction() {
|
|
135
135
|
throw new McpError(-33003, 'View changed; refresh before acting', { 'native-ui/action': { status: 'not-applied', reason: 'stale-view' } });
|
|
136
136
|
}
|
|
137
|
-
function
|
|
137
|
+
function secondaryActions(meta) {
|
|
138
|
+
return z.object({ footerSecondaryActions: z.literal(true) }).safeParse(meta?.['native-ui/features']).success;
|
|
139
|
+
}
|
|
140
|
+
function view(data, scoped = false, secondary = false) {
|
|
138
141
|
const { tasks, task, config, revision } = data;
|
|
139
142
|
const state = Context.parse({ conversationId: 'outgoing-state', state: { ...data.state, selectedId: task?.id ?? null } }).state;
|
|
140
143
|
const controls = [];
|
|
@@ -147,7 +150,7 @@ function view(data, scoped = false) {
|
|
|
147
150
|
action('done', 'Complete task', { icon: icons.check });
|
|
148
151
|
}
|
|
149
152
|
else if (state.proposal) {
|
|
150
|
-
action('proposal:accept', 'Accept suggestion', { icon: icons.add, description: state.proposal.text });
|
|
153
|
+
action('proposal:accept', 'Accept suggestion', { icon: icons.add, description: `Left-click to create/start/select; right-click to dismiss.\n${state.proposal.text}` });
|
|
151
154
|
action('proposal:dismiss', 'Dismiss suggestion', { icon: icons.cross });
|
|
152
155
|
}
|
|
153
156
|
action('screen:tasks', 'Tasks', { icon: icons.note });
|
|
@@ -196,7 +199,7 @@ function view(data, scoped = false) {
|
|
|
196
199
|
footer.push({ text: `${task.tag ? `[${task.tag}] ` : ''}${task.text.split('\n')[0].slice(0, 2000)}`, action: 'preview', width: config.config.titleWidth });
|
|
197
200
|
}
|
|
198
201
|
else if (state.proposal) {
|
|
199
|
-
footer.push({ text: `${icons.
|
|
202
|
+
footer.push({ text: `${icons.add} `, action: 'proposal:accept', ...(secondary ? { secondaryAction: 'proposal:dismiss' } : {}), width: 3 });
|
|
200
203
|
footer.push({ text: `${state.proposal.tag ? `[${state.proposal.tag}] ` : ''}${state.proposal.text.split('\n')[0].slice(0, 2000)}`, width: config.config.titleWidth });
|
|
201
204
|
}
|
|
202
205
|
else
|
|
@@ -253,7 +256,7 @@ server.setRequestHandler(ReadResourceRequestSchema, (request, extra) => serial(a
|
|
|
253
256
|
if (request.params.uri !== URI)
|
|
254
257
|
throw new Error('Unknown resource');
|
|
255
258
|
const data = await snapshot(context(request.params._meta).state, extra.signal);
|
|
256
|
-
return { contents: [{ uri: URI, mimeType: MIME, text: JSON.stringify(view(data, scopedActions(request.params._meta))) }] };
|
|
259
|
+
return { contents: [{ uri: URI, mimeType: MIME, text: JSON.stringify(view(data, scopedActions(request.params._meta), secondaryActions(request.params._meta))) }] };
|
|
257
260
|
}));
|
|
258
261
|
server.setRequestHandler(Action, (request, extra) => serial(async () => {
|
|
259
262
|
const state = context(request.params._meta).state;
|
|
@@ -351,13 +354,13 @@ server.setRequestHandler(Action, (request, extra) => serial(async () => {
|
|
|
351
354
|
extra.signal.throwIfAborted();
|
|
352
355
|
saveFooterConfig(cfg, before.config.raw, handoff, newSession, policy);
|
|
353
356
|
}
|
|
354
|
-
const result = { view: view(await snapshot(next, extra.signal), scoped), ...(draft ? { draft } : {}), ...(preview ? { preview } : {}) };
|
|
357
|
+
const result = { view: view(await snapshot(next, extra.signal), scoped, secondaryActions(request.params._meta)), ...(draft ? { draft } : {}), ...(preview ? { preview } : {}) };
|
|
355
358
|
await server.notification({ method: 'notifications/native-ui/changed', params: { uri: URI } });
|
|
356
359
|
return result;
|
|
357
360
|
}));
|
|
358
361
|
const empty = { type: 'object', properties: {}, additionalProperties: false };
|
|
359
362
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [
|
|
360
|
-
{ name: 'propose', description: 'Before multi-step implementation or configuration migrations, suggest a task if get_current returns null; skip informational questions and trivial edits. Show a short action-oriented title, optional details after a blank line, and reuse a saved tag from tags. The user clicks + to create/start/select or dismiss. Replaces an unaccepted suggestion; continue working without waiting or asking again. Never creates a task before a user action.', inputSchema: { type: 'object', properties: { text: { type: 'string', minLength: 1, maxLength: 4000 }, tag: { type: 'string', minLength: 1, maxLength: 64 } }, required: ['text'], additionalProperties: false } },
|
|
363
|
+
{ name: 'propose', description: 'Before multi-step implementation or configuration migrations, suggest a task if get_current returns null; skip informational questions and trivial edits. Show a short action-oriented title, optional details after a blank line, and reuse a saved tag from tags. The user left-clicks + to create/start/select or right-clicks it to dismiss (also available in the app menu). Replaces an unaccepted suggestion; continue working without waiting or asking again. Never creates a task before a user action.', inputSchema: { type: 'object', properties: { text: { type: 'string', minLength: 1, maxLength: 4000 }, tag: { type: 'string', minLength: 1, maxLength: 64 } }, required: ['text'], additionalProperties: false } },
|
|
361
364
|
{ name: 'get_current', description: 'Read the task selected through the native Pinote UI for this conversation before multi-step implementation or configuration migrations. Returns null if unselected; then use tags and propose to suggest a task without creating one. Skip informational questions and trivial edits; never replace an existing selection.', inputSchema: empty, annotations: { readOnlyHint: true } },
|
|
362
365
|
{ name: 'fields', description: 'Read global footer field definitions before setting task handoff values. Does not edit settings.', inputSchema: empty, annotations: { readOnlyHint: true } },
|
|
363
366
|
{ name: 'tags', description: 'List saved Pinote tags.', inputSchema: empty, annotations: { readOnlyHint: true } },
|
|
@@ -376,7 +379,7 @@ server.setRequestHandler(CallToolRequestSchema, (request, extra) => serial(async
|
|
|
376
379
|
outgoing = boundedState({ ...state, selectedId: null, proposal: { ...args, id: randomUUID() } });
|
|
377
380
|
if (state.proposal && !await consumed(state.proposal.id))
|
|
378
381
|
await consume(state.proposal.id);
|
|
379
|
-
result = { status: 'Suggested; user can accept
|
|
382
|
+
result = { status: 'Suggested; user can left-click + to accept or right-click + to dismiss (or use the app menu). Continue working without asking again.' };
|
|
380
383
|
}
|
|
381
384
|
else if (request.params.name === 'get_current')
|
|
382
385
|
result = await selected(state, extra.signal);
|
package/mcp/README.md
CHANGED
|
@@ -55,10 +55,14 @@ normal personal data. No desktop notifications are sent.
|
|
|
55
55
|
fails closed; repair it manually rather than replacing it with defaults.
|
|
56
56
|
- **Footer:** selected task plus configured fields; safe HTTP(S) links are clickable.
|
|
57
57
|
The title previews the task; **✔** completes it and clears the selection without
|
|
58
|
-
resetting the conversation or editor. Suggestions show
|
|
59
|
-
create/start/select
|
|
58
|
+
resetting the conversation or editor. Suggestions show only **✚**: left-click to
|
|
59
|
+
create/start/select, right-click to dismiss. Right-click requires gateway 0.22.0+
|
|
60
|
+
and its Kitty mouse mapping; **Dismiss suggestion** in the app menu remains the
|
|
61
|
+
keyboard/older-host fallback. These use the gateway's generic footer actions, not a Pinote
|
|
60
62
|
extension or private handler. Install the gateway's Kitty protocol handler as
|
|
61
|
-
described in its native UI documentation
|
|
63
|
+
and right-click mouse mapping described in its native UI documentation. Existing
|
|
64
|
+
installations need to add the mapping and reload Kitty; package updates do not
|
|
65
|
+
edit terminal configuration. App menus remain the keyboard fallback.
|
|
62
66
|
- **Agent:** `propose`, `get_current`, `update_current`, `fields`, and `tags` are
|
|
63
67
|
available through gateway tool discovery. `propose` returns opaque UI state;
|
|
64
68
|
it does not create a task. It replaces an unaccepted suggestion only when no task
|
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.
|
|
35
|
+
const server = new Server({ name: 'pinote', version: '0.22.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> {
|
|
@@ -115,7 +115,10 @@ function actionRevision(data: Snapshot, control: Control): string {
|
|
|
115
115
|
function staleAction(): never {
|
|
116
116
|
throw new McpError(-33003, 'View changed; refresh before acting', { 'native-ui/action': { status: 'not-applied', reason: 'stale-view' } });
|
|
117
117
|
}
|
|
118
|
-
function
|
|
118
|
+
function secondaryActions(meta: Record<string, unknown> | undefined): boolean {
|
|
119
|
+
return z.object({ footerSecondaryActions: z.literal(true) }).safeParse(meta?.['native-ui/features']).success;
|
|
120
|
+
}
|
|
121
|
+
function view(data: Snapshot, scoped = false, secondary = false) {
|
|
119
122
|
const { tasks, task, config, revision } = data;
|
|
120
123
|
const state = Context.parse({ conversationId: 'outgoing-state', state: { ...data.state, selectedId: task?.id ?? null } }).state;
|
|
121
124
|
const controls: Control[] = [];
|
|
@@ -127,7 +130,7 @@ function view(data: Snapshot, scoped = false) {
|
|
|
127
130
|
action('preview', 'Preview full task', { description: 'Display only; never sent to the model' });
|
|
128
131
|
action('done', 'Complete task', { icon: icons.check });
|
|
129
132
|
} else if (state.proposal) {
|
|
130
|
-
action('proposal:accept', 'Accept suggestion', { icon: icons.add, description: state.proposal.text });
|
|
133
|
+
action('proposal:accept', 'Accept suggestion', { icon: icons.add, description: `Left-click to create/start/select; right-click to dismiss.\n${state.proposal.text}` });
|
|
131
134
|
action('proposal:dismiss', 'Dismiss suggestion', { icon: icons.cross });
|
|
132
135
|
}
|
|
133
136
|
action('screen:tasks', 'Tasks', { icon: icons.note });
|
|
@@ -161,12 +164,12 @@ function view(data: Snapshot, scoped = false) {
|
|
|
161
164
|
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?' });
|
|
162
165
|
if (matches.length > 200) body += '\nShowing the first 200 matches; use Filter tasks to narrow the list.';
|
|
163
166
|
}
|
|
164
|
-
const footer: { text: string; url?: string; action?: string; width?: number }[] = [{ text: `${icons.note} `, width: 3 }];
|
|
167
|
+
const footer: { text: string; url?: string; action?: string; secondaryAction?: string; width?: number }[] = [{ text: `${icons.note} `, width: 3 }];
|
|
165
168
|
if (task) {
|
|
166
169
|
footer.push({ text: `${icons.check} `, action: 'done', width: 3 });
|
|
167
170
|
footer.push({ text: `${task.tag ? `[${task.tag}] ` : ''}${task.text.split('\n')[0].slice(0, 2000)}`, action: 'preview', width: config.config.titleWidth });
|
|
168
171
|
} else if (state.proposal) {
|
|
169
|
-
footer.push({ text: `${icons.
|
|
172
|
+
footer.push({ text: `${icons.add} `, action: 'proposal:accept', ...(secondary ? { secondaryAction: 'proposal:dismiss' } : {}), width: 3 });
|
|
170
173
|
footer.push({ text: `${state.proposal.tag ? `[${state.proposal.tag}] ` : ''}${state.proposal.text.split('\n')[0].slice(0, 2000)}`, width: config.config.titleWidth });
|
|
171
174
|
} else footer.push({ text: 'Pinote · no task', width: config.config.titleWidth });
|
|
172
175
|
const titleParts = footer.length;
|
|
@@ -208,7 +211,7 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [
|
|
|
208
211
|
server.setRequestHandler(ReadResourceRequestSchema, (request, extra) => serial(async () => {
|
|
209
212
|
if (request.params.uri !== URI) throw new Error('Unknown resource');
|
|
210
213
|
const data = await snapshot(context(request.params._meta).state, extra.signal);
|
|
211
|
-
return { contents: [{ uri: URI, mimeType: MIME, text: JSON.stringify(view(data, scopedActions(request.params._meta))) }] };
|
|
214
|
+
return { contents: [{ uri: URI, mimeType: MIME, text: JSON.stringify(view(data, scopedActions(request.params._meta), secondaryActions(request.params._meta))) }] };
|
|
212
215
|
}));
|
|
213
216
|
server.setRequestHandler(Action, (request, extra) => serial(async () => {
|
|
214
217
|
const state = context(request.params._meta).state;
|
|
@@ -272,13 +275,13 @@ server.setRequestHandler(Action, (request, extra) => serial(async () => {
|
|
|
272
275
|
extra.signal.throwIfAborted();
|
|
273
276
|
saveFooterConfig(cfg, before.config.raw, handoff, newSession, policy);
|
|
274
277
|
}
|
|
275
|
-
const result = { view: view(await snapshot(next, extra.signal), scoped), ...(draft ? { draft } : {}), ...(preview ? { preview } : {}) };
|
|
278
|
+
const result = { view: view(await snapshot(next, extra.signal), scoped, secondaryActions(request.params._meta)), ...(draft ? { draft } : {}), ...(preview ? { preview } : {}) };
|
|
276
279
|
await server.notification({ method: 'notifications/native-ui/changed', params: { uri: URI } });
|
|
277
280
|
return result;
|
|
278
281
|
}));
|
|
279
282
|
const empty = { type: 'object' as const, properties: {}, additionalProperties: false };
|
|
280
283
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [
|
|
281
|
-
{ name: 'propose', description: 'Before multi-step implementation or configuration migrations, suggest a task if get_current returns null; skip informational questions and trivial edits. Show a short action-oriented title, optional details after a blank line, and reuse a saved tag from tags. The user clicks + to create/start/select or dismiss. Replaces an unaccepted suggestion; continue working without waiting or asking again. Never creates a task before a user action.', inputSchema: { type: 'object' as const, properties: { text: { type: 'string', minLength: 1, maxLength: 4000 }, tag: { type: 'string', minLength: 1, maxLength: 64 } }, required: ['text'], additionalProperties: false } },
|
|
284
|
+
{ name: 'propose', description: 'Before multi-step implementation or configuration migrations, suggest a task if get_current returns null; skip informational questions and trivial edits. Show a short action-oriented title, optional details after a blank line, and reuse a saved tag from tags. The user left-clicks + to create/start/select or right-clicks it to dismiss (also available in the app menu). Replaces an unaccepted suggestion; continue working without waiting or asking again. Never creates a task before a user action.', inputSchema: { type: 'object' as const, properties: { text: { type: 'string', minLength: 1, maxLength: 4000 }, tag: { type: 'string', minLength: 1, maxLength: 64 } }, required: ['text'], additionalProperties: false } },
|
|
282
285
|
{ name: 'get_current', description: 'Read the task selected through the native Pinote UI for this conversation before multi-step implementation or configuration migrations. Returns null if unselected; then use tags and propose to suggest a task without creating one. Skip informational questions and trivial edits; never replace an existing selection.', inputSchema: empty, annotations: { readOnlyHint: true } },
|
|
283
286
|
{ name: 'fields', description: 'Read global footer field definitions before setting task handoff values. Does not edit settings.', inputSchema: empty, annotations: { readOnlyHint: true } },
|
|
284
287
|
{ name: 'tags', description: 'List saved Pinote tags.', inputSchema: empty, annotations: { readOnlyHint: true } },
|
|
@@ -294,7 +297,7 @@ server.setRequestHandler(CallToolRequestSchema, (request, extra) => serial(async
|
|
|
294
297
|
if (!args.text.trim()) throw new Error('Task text must not be blank');
|
|
295
298
|
outgoing = boundedState({ ...state, selectedId: null, proposal: { ...args, id: randomUUID() } });
|
|
296
299
|
if (state.proposal && !await consumed(state.proposal.id)) await consume(state.proposal.id);
|
|
297
|
-
result = { status: 'Suggested; user can accept
|
|
300
|
+
result = { status: 'Suggested; user can left-click + to accept or right-click + to dismiss (or use the app menu). Continue working without asking again.' };
|
|
298
301
|
} else if (request.params.name === 'get_current') result = await selected(state, extra.signal);
|
|
299
302
|
else if (request.params.name === 'fields') result = settings().config;
|
|
300
303
|
else if (request.params.name === 'tags') result = JSON.parse(await note(['agent', 'tags'], extra.signal));
|