pi-note 0.30.0 → 0.31.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 +8 -0
- package/dist-mcp/mcp/core.js +10 -3
- package/mcp/core.ts +10 -3
- package/package.json +1 -1
- package/remote.ts +28 -3
package/README.md
CHANGED
|
@@ -74,6 +74,14 @@ Unrelated fresh launches start unselected.
|
|
|
74
74
|
Changing endpoints starts a separate selection namespace. Unsaved Pi sessions
|
|
75
75
|
cannot guarantee selection persistence.
|
|
76
76
|
|
|
77
|
+
The GTK app's **Start in Kitty…** action (note 0.13.0+) chooses a working directory
|
|
78
|
+
and opens a named tab with this task selected, without submitting a prompt. Client
|
|
79
|
+
and service both need pi-note 0.31.0+. The one-shot `PINOTE_LAUNCH_TASK` environment
|
|
80
|
+
handoff carries only the task ID and displayed revision, never task text or tokens.
|
|
81
|
+
Only a fresh session can consume it. The service must share the GUI's database;
|
|
82
|
+
missing/stale revisions fail closed. Resume, reload and `/new` never replay the
|
|
83
|
+
handoff or replace another selection. Pi keeps its normal endpoint/token settings.
|
|
84
|
+
|
|
77
85
|
### Agent tools and context
|
|
78
86
|
|
|
79
87
|
The extension registers `pinote_search`, `pinote_get_task`, `pinote_get_current`,
|
package/dist-mcp/mcp/core.js
CHANGED
|
@@ -29,6 +29,7 @@ const ProposalInput = z.union([NewProposal, ExistingProposal]);
|
|
|
29
29
|
const Proposal = z.union([NewProposal.extend({ id: z.string().uuid() }), ExistingProposal.extend({ id: z.string().uuid() })]);
|
|
30
30
|
const Context = z.object({ conversationId: z.string().min(1).max(256), contextId: z.string().min(1).max(256).optional(), state: z.object({
|
|
31
31
|
proposal: Proposal.nullable().optional(),
|
|
32
|
+
launch: z.object({ task_id: TaskId, updated_at: z.string().min(1).max(128) }).strict().optional(),
|
|
32
33
|
selectedId: z.number().int().positive().safe().nullable().optional(), screen: z.enum(['tasks', 'settings', 'field']).optional(),
|
|
33
34
|
field: z.string().refine((value) => [...value].length <= 64, 'Field name exceeds 64 code points').optional(), search: z.string().max(256).optional(),
|
|
34
35
|
}).default({}) });
|
|
@@ -128,7 +129,10 @@ async function selection(state, signal) {
|
|
|
128
129
|
return { task, proposal, proposedTask };
|
|
129
130
|
}
|
|
130
131
|
async function snapshot(state, signal) {
|
|
131
|
-
const
|
|
132
|
+
const launchTask = state.launch ? JSON.parse(await note(['agent', 'get', String(state.launch.task_id)], signal)) : null;
|
|
133
|
+
const tasks = state.launch
|
|
134
|
+
? launchTask && ['active', 'in_progress'].includes(launchTask.state) ? [launchTask] : []
|
|
135
|
+
: JSON.parse(await note(['list', '--json'], signal));
|
|
132
136
|
const { task, proposal, proposedTask } = await selection(state, signal);
|
|
133
137
|
const config = settings();
|
|
134
138
|
const suggestion = proposal && ('task_id' in proposal ? proposedTask : proposal);
|
|
@@ -223,7 +227,9 @@ function view(data, scoped = false, secondary = false, features = {}) {
|
|
|
223
227
|
}
|
|
224
228
|
controls.push({ id: 'add', label: 'New task', icon: icons.add, kind: 'text', multiline: true, value: '', confirm: 'Create and select this task?' });
|
|
225
229
|
controls.push({ id: 'search', label: 'Filter tasks', kind: 'text', value: state.search ?? '' });
|
|
226
|
-
const matches = tasks.filter((t) =>
|
|
230
|
+
const matches = tasks.filter((t) => state.launch
|
|
231
|
+
? t.id === state.launch.task_id && t.updated_at === state.launch.updated_at
|
|
232
|
+
: `${t.text} ${t.tag ?? ''}`.toLowerCase().includes((state.search ?? '').toLowerCase()));
|
|
227
233
|
for (const candidate of matches.slice(0, 200))
|
|
228
234
|
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?' });
|
|
229
235
|
if (matches.length > 200)
|
|
@@ -362,6 +368,7 @@ export function createPinoteServer(options = {}) {
|
|
|
362
368
|
if (!control || (control.revision ?? before.revision) !== request.params.revision)
|
|
363
369
|
staleAction();
|
|
364
370
|
const next = { ...before.state };
|
|
371
|
+
delete next.launch; // A launcher filter is one-shot; ordinary navigation restores the list.
|
|
365
372
|
let draft, preview;
|
|
366
373
|
if (action === 'proposal:accept' || action === 'proposal:dismiss') {
|
|
367
374
|
const proposal = before.state.proposal;
|
|
@@ -395,7 +402,7 @@ export function createPinoteServer(options = {}) {
|
|
|
395
402
|
if (!candidate)
|
|
396
403
|
staleAction();
|
|
397
404
|
const target = JSON.parse(await note(['agent', 'get', String(id)], extra.signal));
|
|
398
|
-
if (target.text !== candidate.text || target.state !== candidate.state || target.tag !== candidate.tag)
|
|
405
|
+
if (target.text !== candidate.text || target.state !== candidate.state || target.tag !== candidate.tag || state.launch && target.updated_at !== state.launch.updated_at)
|
|
399
406
|
staleAction();
|
|
400
407
|
if (next.proposal)
|
|
401
408
|
await consume(next.proposal.id);
|
package/mcp/core.ts
CHANGED
|
@@ -30,6 +30,7 @@ const ProposalInput = z.union([NewProposal, ExistingProposal]);
|
|
|
30
30
|
const Proposal = z.union([NewProposal.extend({ id: z.string().uuid() }), ExistingProposal.extend({ id: z.string().uuid() })]);
|
|
31
31
|
const Context = z.object({ conversationId: z.string().min(1).max(256), contextId: z.string().min(1).max(256).optional(), state: z.object({
|
|
32
32
|
proposal: Proposal.nullable().optional(),
|
|
33
|
+
launch: z.object({ task_id: TaskId, updated_at: z.string().min(1).max(128) }).strict().optional(),
|
|
33
34
|
selectedId: z.number().int().positive().safe().nullable().optional(), screen: z.enum(['tasks', 'settings', 'field']).optional(),
|
|
34
35
|
field: z.string().refine((value) => [...value].length <= 64, 'Field name exceeds 64 code points').optional(), search: z.string().max(256).optional(),
|
|
35
36
|
}).default({}) });
|
|
@@ -110,7 +111,10 @@ async function selection(state: State, signal: AbortSignal) {
|
|
|
110
111
|
return { task, proposal, proposedTask };
|
|
111
112
|
}
|
|
112
113
|
async function snapshot(state: State, signal: AbortSignal) {
|
|
113
|
-
const
|
|
114
|
+
const launchTask: Task | null = state.launch ? JSON.parse(await note(['agent', 'get', String(state.launch.task_id)], signal)) : null;
|
|
115
|
+
const tasks: Task[] = state.launch
|
|
116
|
+
? launchTask && ['active', 'in_progress'].includes(launchTask.state) ? [launchTask] : []
|
|
117
|
+
: JSON.parse(await note(['list', '--json'], signal));
|
|
114
118
|
const { task, proposal, proposedTask } = await selection(state, signal);
|
|
115
119
|
const config = settings();
|
|
116
120
|
const suggestion = proposal && ('task_id' in proposal ? proposedTask : proposal);
|
|
@@ -193,7 +197,9 @@ function view(data: Snapshot, scoped = false, secondary = false, features: { foo
|
|
|
193
197
|
}
|
|
194
198
|
controls.push({ id: 'add', label: 'New task', icon: icons.add, kind: 'text', multiline: true, value: '', confirm: 'Create and select this task?' });
|
|
195
199
|
controls.push({ id: 'search', label: 'Filter tasks', kind: 'text', value: state.search ?? '' });
|
|
196
|
-
const matches = tasks.filter((t) =>
|
|
200
|
+
const matches = tasks.filter((t) => state.launch
|
|
201
|
+
? t.id === state.launch.task_id && t.updated_at === state.launch.updated_at
|
|
202
|
+
: `${t.text} ${t.tag ?? ''}`.toLowerCase().includes((state.search ?? '').toLowerCase()));
|
|
197
203
|
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?' });
|
|
198
204
|
if (matches.length > 200) body += '\nShowing the first 200 matches; use Filter tasks to narrow the list.';
|
|
199
205
|
}
|
|
@@ -303,6 +309,7 @@ server.setRequestHandler(Action, (request, extra) => serial(async () => {
|
|
|
303
309
|
const control = view(before, scoped, secondaryActions(request.params._meta), footerFeatures(request.params._meta)).controls.find((item) => item.id === action);
|
|
304
310
|
if (!control || (control.revision ?? before.revision) !== request.params.revision) staleAction();
|
|
305
311
|
const next: State = { ...before.state };
|
|
312
|
+
delete next.launch; // A launcher filter is one-shot; ordinary navigation restores the list.
|
|
306
313
|
let draft: string | undefined, preview: string | undefined;
|
|
307
314
|
if (action === 'proposal:accept' || action === 'proposal:dismiss') {
|
|
308
315
|
const proposal = before.state.proposal;
|
|
@@ -324,7 +331,7 @@ server.setRequestHandler(Action, (request, extra) => serial(async () => {
|
|
|
324
331
|
const candidate = before.tasks.find((task) => task.id === id);
|
|
325
332
|
if (!candidate) staleAction();
|
|
326
333
|
const target: Task = JSON.parse(await note(['agent', 'get', String(id)], extra.signal));
|
|
327
|
-
if (target.text !== candidate.text || target.state !== candidate.state || target.tag !== candidate.tag) staleAction();
|
|
334
|
+
if (target.text !== candidate.text || target.state !== candidate.state || target.tag !== candidate.tag || state.launch && target.updated_at !== state.launch.updated_at) staleAction();
|
|
328
335
|
if (next.proposal) await consume(next.proposal.id);
|
|
329
336
|
await note(['agent', 'start', String(id), '--expected-updated-at', target.updated_at], extra.signal); next.selectedId = id; next.proposal = null;
|
|
330
337
|
} else if (action === 'add') {
|
package/package.json
CHANGED
package/remote.ts
CHANGED
|
@@ -14,6 +14,8 @@ const transfers: Map<string, SavedState> = ((globalThis as any)[key] ??= new Map
|
|
|
14
14
|
export default function pinoteRemote(pi: ExtensionAPI): void {
|
|
15
15
|
let session: RemoteSession | undefined, ui: RemoteUI | undefined;
|
|
16
16
|
let file: string | undefined;
|
|
17
|
+
let launchRequest = process.env.PINOTE_LAUNCH_TASK;
|
|
18
|
+
delete process.env.PINOTE_LAUNCH_TASK; // Never inherit consent into another Pi process or /reload.
|
|
17
19
|
const getSession = () => { if (!session) throw new RemoteFailure(); return session; };
|
|
18
20
|
registerTools(pi, getSession);
|
|
19
21
|
registerPreview(pi);
|
|
@@ -34,6 +36,7 @@ export default function pinoteRemote(pi: ExtensionAPI): void {
|
|
|
34
36
|
pi.registerCommand('pinote', { description: 'Open remote Pinote tasks and settings', handler: open });
|
|
35
37
|
pi.registerCommand('pi-note', { description: 'Open remote Pinote (compatibility alias)', handler: open });
|
|
36
38
|
const start = async (ctx: ExtensionContext, reason?: string, previous?: string) => {
|
|
39
|
+
const request = launchRequest; launchRequest = undefined; // Never replay an uncertain start.
|
|
37
40
|
ui?.detach(); await session?.stop();
|
|
38
41
|
ui = undefined; session = undefined;
|
|
39
42
|
try {
|
|
@@ -42,12 +45,34 @@ export default function pinoteRemote(pi: ExtensionAPI): void {
|
|
|
42
45
|
ui = new RemoteUI(pi, session);
|
|
43
46
|
ui.attach(ctx);
|
|
44
47
|
file = ctx.sessionManager.getSessionFile();
|
|
45
|
-
const
|
|
46
|
-
|
|
48
|
+
const branch = ctx.sessionManager.getBranch();
|
|
49
|
+
let carried = reason === 'new' && previous ? transfers.get(previous) : undefined;
|
|
50
|
+
let launch: { task_id: number; updated_at: string } | undefined;
|
|
51
|
+
if (request) {
|
|
52
|
+
try {
|
|
53
|
+
if (reason !== 'startup' || branch.some((entry) => entry.type === 'message' || entry.type === 'custom' && entry.customType === ENTRY_TYPE)) throw new Error('Launch requires a fresh Pi session');
|
|
54
|
+
const parsed = JSON.parse(request);
|
|
55
|
+
if (!parsed || !Number.isSafeInteger(parsed.task_id) || parsed.task_id < 1 ||
|
|
56
|
+
typeof parsed.updated_at !== 'string' || !parsed.updated_at.length || parsed.updated_at.length > 128) throw new Error('Invalid launch request');
|
|
57
|
+
launch = { task_id: parsed.task_id, updated_at: parsed.updated_at };
|
|
58
|
+
carried = { scope: config.scope, contextId: randomUUID(), state: { launch } };
|
|
59
|
+
} catch {
|
|
60
|
+
ctx.ui.notify('Pinote launch ignored: invalid request or nonempty/resumed session. Existing selection is unchanged.', 'warning');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
await session.start(branch, ctx.sessionManager.getSessionId() || randomUUID(), carried);
|
|
64
|
+
if (launch) {
|
|
65
|
+
const view = session.view!;
|
|
66
|
+
// The service exposes this control only for the exact GUI-displayed revision.
|
|
67
|
+
if (JSON.stringify(view.state.launch) !== JSON.stringify(launch)) throw new Error('Service does not support launch handoffs');
|
|
68
|
+
const control = view.controls.find((item) => item.id === `select:${launch.task_id}`);
|
|
69
|
+
if (!control) throw new Error('Launch task changed or belongs to another service');
|
|
70
|
+
await session.action(view, control); // The directory chooser's Start click granted consent.
|
|
71
|
+
}
|
|
47
72
|
if (reason === 'new' && previous) transfers.delete(previous);
|
|
48
73
|
} catch (error) {
|
|
49
74
|
// Configuration messages are fixed strings; transport response bodies are never shown.
|
|
50
|
-
ctx.ui.notify(error instanceof RemoteFailure ? error.message : 'Pinote unavailable: check
|
|
75
|
+
ctx.ui.notify(error instanceof RemoteFailure ? error.message : 'Pinote unavailable or launch task changed: check the service, task revision and private token configuration. Use /pinote to select manually; launch requests are never retried.', 'warning');
|
|
51
76
|
}
|
|
52
77
|
};
|
|
53
78
|
pi.on('session_start', (event, ctx) => start(ctx, event.reason, event.previousSessionFile));
|