pi-note 0.30.0 → 0.31.1
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 +12 -3
- package/dist-mcp/mcp/core.js +11 -4
- package/mcp/core.ts +11 -4
- package/package.json +1 -1
- package/remote/ui.ts +1 -1
- package/remote.ts +28 -3
package/README.md
CHANGED
|
@@ -50,9 +50,10 @@ state routing, tools and UI.
|
|
|
50
50
|
|
|
51
51
|
Open **`/pinote`** (`/pi-note` is an alias). Select or create a task, preview it,
|
|
52
52
|
clear the selection, complete it, respond to suggestions, or edit shared settings.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
Selecting an existing task starts it immediately and returns focus to the prompt,
|
|
54
|
+
with no extra confirmation. Creation and settings changes still require confirmation.
|
|
55
|
+
Nothing submits a model prompt automatically; inserting Continue's draft requires
|
|
56
|
+
confirmation and preserves an existing draft.
|
|
56
57
|
|
|
57
58
|
Menus use Pi's native components and configured selection keys. **Escape** goes
|
|
58
59
|
back; **Ctrl+R** refreshes. Settings with fewer than five choices cycle inline.
|
|
@@ -74,6 +75,14 @@ Unrelated fresh launches start unselected.
|
|
|
74
75
|
Changing endpoints starts a separate selection namespace. Unsaved Pi sessions
|
|
75
76
|
cannot guarantee selection persistence.
|
|
76
77
|
|
|
78
|
+
The GTK app's **Start in Kitty…** action (note 0.13.0+) chooses a working directory
|
|
79
|
+
and opens a named tab with this task selected, without submitting a prompt. Client
|
|
80
|
+
and service both need pi-note 0.31.0+. The one-shot `PINOTE_LAUNCH_TASK` environment
|
|
81
|
+
handoff carries only the task ID and displayed revision, never task text or tokens.
|
|
82
|
+
Only a fresh session can consume it. The service must share the GUI's database;
|
|
83
|
+
missing/stale revisions fail closed. Resume, reload and `/new` never replay the
|
|
84
|
+
handoff or replace another selection. Pi keeps its normal endpoint/token settings.
|
|
85
|
+
|
|
77
86
|
### Agent tools and context
|
|
78
87
|
|
|
79
88
|
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,9 +227,11 @@ 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
|
-
action(`select:${candidate.id}`, `${candidate.tag ? `[${candidate.tag}] ` : ''}${candidate.text.split('\n')[0]}`, { description: `#${candidate.id} · ${candidate.state}
|
|
234
|
+
action(`select:${candidate.id}`, `${candidate.tag ? `[${candidate.tag}] ` : ''}${candidate.text.split('\n')[0]}`, { description: `#${candidate.id} · ${candidate.state} · Select and start this task` });
|
|
229
235
|
if (matches.length > 200)
|
|
230
236
|
body += '\nShowing the first 200 matches; use Filter tasks to narrow the list.';
|
|
231
237
|
}
|
|
@@ -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,8 +197,10 @@ 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) =>
|
|
197
|
-
|
|
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()));
|
|
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} · Select and start this task` });
|
|
198
204
|
if (matches.length > 200) body += '\nShowing the first 200 matches; use Filter tasks to narrow the list.';
|
|
199
205
|
}
|
|
200
206
|
const footer: { text: string; openApp?: true; url?: string; action?: string; secondaryAction?: string; doubleAction?: string; style?: { italic: boolean; color: 'accent' }; width?: number }[] = features.footerNativeActions
|
|
@@ -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/ui.ts
CHANGED
|
@@ -172,7 +172,7 @@ export class RemoteUI {
|
|
|
172
172
|
ctx.ui.notify('Value does not match the field type.', 'warning'); continue;
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
|
-
if (current()
|
|
175
|
+
if (current() && await this.dispatch(ctx, view, control, value) && control.id.startsWith('select:')) return;
|
|
176
176
|
}
|
|
177
177
|
} catch (error) { if (current()) ctx.ui.notify(error instanceof RemoteFailure ? error.message : 'Pinote unavailable', 'error'); }
|
|
178
178
|
finally { this.opening = false; }
|
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));
|