pi-note 0.39.3 → 0.39.4

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
@@ -13,6 +13,10 @@ Host Pi ── Pinote extension ──┐
13
13
  VM Pi ──── Pinote extension ──┘
14
14
  ```
15
15
 
16
+ Compact suggestion and selected-task titles hide leading Markdown heading markers
17
+ (`# ` through `###### `); saved task text and
18
+ full Markdown previews keep the original source.
19
+
16
20
  Tasks, tags, progress and handoff fields are shared. Selection and pending
17
21
  suggestions belong to each Pi conversation/branch, not the HTTP connection.
18
22
  Other tools continue running wherever Pi and their services run.
@@ -13,6 +13,7 @@ import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
13
13
  import { stripVTControlCharacters } from 'node:util';
14
14
  import { defaultAgentGuidance, defaultFooterField, defaultHandoffPrompt, defaultNewSessionPrompt, effectiveFooterFields, parseAgentGuidance, parseFooterConfig, parseHandoffPrompt, parseNewSessionPrompt, readConfigDocument, readFooterDocument, saveFooterConfig } from "../footer-config.js";
15
15
  import { proposalTitleOptions, proposalDescriptionOptions, proposalText } from "../proposal.js";
16
+ import { taskTitle } from "../task-title.js";
16
17
  const URI = 'pinote://ui';
17
18
  const MIME = 'application/vnd.mcp-native-ui+json';
18
19
  const AGENT_URI = 'pinote://agent-context';
@@ -195,7 +196,7 @@ function view(data, scoped = false, secondary = false, features = {}) {
195
196
  const controls = [];
196
197
  const action = (id, label, extra = {}) => controls.push({ id, label: label.slice(0, 2000), kind: 'action', ...extra });
197
198
  let title = 'Pinote · Tasks';
198
- let body = task ? `#${task.id} · ${task.tag || 'No tag'} · ${humanStatus(task.state)} · CURRENT\n${task.text.split('\n')[0]}` : state.proposal ? `Suggestion only; no task is selected for this conversation.\n${suggestion.text.split('\n')[0]}` : 'Select a task or create one. No task is selected for this conversation.';
199
+ let body = task ? `#${task.id} · ${task.tag || 'No tag'} · ${humanStatus(task.state)} · CURRENT\n${taskTitle(task.text)}` : state.proposal ? `Suggestion only; no task is selected for this conversation.\n${taskTitle(suggestion.text)}` : 'Select a task or create one. No task is selected for this conversation.';
199
200
  // Footer actions remain valid on every screen, including Settings.
200
201
  if (task) {
201
202
  action('preview', 'Preview full task', { description: 'Display only; never sent to the model' });
@@ -253,7 +254,7 @@ function view(data, scoped = false, secondary = false, features = {}) {
253
254
  ? t.id === state.launch.task_id && t.updated_at === state.launch.updated_at
254
255
  : terms.every((term) => `${t.text} ${t.tag ?? ''} #${t.id}`.toLowerCase().includes(term)));
255
256
  for (const candidate of matches.slice(0, 200))
256
- action(`select:${candidate.id}`, candidate.text.split('\n')[0], { description: `#${candidate.id} · ${candidate.tag || 'No tag'} · ${humanStatus(candidate.state)}${candidate.id === task?.id ? ' · CURRENT' : ''} · Select and start this task for this conversation` });
257
+ action(`select:${candidate.id}`, taskTitle(candidate.text), { description: `#${candidate.id} · ${candidate.tag || 'No tag'} · ${humanStatus(candidate.state)}${candidate.id === task?.id ? ' · CURRENT' : ''} · Select and start this task for this conversation` });
257
258
  if (matches.length > 200)
258
259
  body += '\nShowing the first 200 matches; use Filter tasks to narrow the list.';
259
260
  }
@@ -262,19 +263,19 @@ function view(data, scoped = false, secondary = false, features = {}) {
262
263
  : [{ text: `${icons.note} `, width: 3 }];
263
264
  if (features.footerNativeActions) {
264
265
  if (task)
265
- 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 });
266
+ footer.push({ text: `${task.tag ? `[${task.tag}] ` : ''}${taskTitle(task.text).slice(0, 2000)}`, action: 'preview', ...(features.footerDoubleActions ? { doubleAction: 'done' } : {}), width: config.config.titleWidth });
266
267
  else if (state.proposal)
267
- footer.push({ text: `? ${suggestion.tag ? `[${suggestion.tag}] ` : ''}${suggestion.text.split('\n')[0].slice(0, 2000)}`, action: 'proposal:preview', ...(features.footerDoubleActions ? { doubleAction: 'proposal:accept' } : {}), ...(features.footerStyles ? { style: { italic: true, color: 'accent' } } : {}), width: config.config.titleWidth });
268
+ footer.push({ text: `? ${suggestion.tag ? `[${suggestion.tag}] ` : ''}${taskTitle(suggestion.text).slice(0, 2000)}`, action: 'proposal:preview', ...(features.footerDoubleActions ? { doubleAction: 'proposal:accept' } : {}), ...(features.footerStyles ? { style: { italic: true, color: 'accent' } } : {}), width: config.config.titleWidth });
268
269
  else
269
270
  footer.push({ text: 'Pinote · No task', width: config.config.titleWidth });
270
271
  }
271
272
  else if (task) {
272
273
  footer.push({ text: `${icons.check} `, action: 'done', width: 3 });
273
- footer.push({ text: `${task.tag ? `[${task.tag}] ` : ''}${task.text.split('\n')[0].slice(0, 2000)}`, action: 'preview', width: config.config.titleWidth });
274
+ footer.push({ text: `${task.tag ? `[${task.tag}] ` : ''}${taskTitle(task.text).slice(0, 2000)}`, action: 'preview', width: config.config.titleWidth });
274
275
  }
275
276
  else if (state.proposal) {
276
277
  footer.push({ text: `${icons.add} `, action: 'proposal:accept', ...(secondary ? { secondaryAction: 'proposal:dismiss' } : {}), width: 3 });
277
- footer.push({ text: `${suggestion.tag ? `[${suggestion.tag}] ` : ''}${suggestion.text.split('\n')[0].slice(0, 2000)}`, action: 'proposal:preview', ...(features.footerStyles ? { style: { italic: true, color: 'accent' } } : {}), width: config.config.titleWidth });
278
+ footer.push({ text: `${suggestion.tag ? `[${suggestion.tag}] ` : ''}${taskTitle(suggestion.text).slice(0, 2000)}`, action: 'proposal:preview', ...(features.footerStyles ? { style: { italic: true, color: 'accent' } } : {}), width: config.config.titleWidth });
278
279
  }
279
280
  else
280
281
  footer.push({ text: 'Pinote · no task', width: config.config.titleWidth });
@@ -641,7 +642,7 @@ export function createPinoteServer(options = {}) {
641
642
  const tasks = JSON.parse(await note(['list', '--json'], extra.signal));
642
643
  const matches = tasks.filter((task) => words.every((word) => `${task.text} ${task.tag ?? ''}`.toLowerCase().includes(word)));
643
644
  const page = matches.slice(args.offset, args.offset + args.limit);
644
- result = { tasks: page.map(({ id, text, state, tag }) => ({ id, title: text.split('\n')[0].slice(0, 1000), state, tag })), total: matches.length, next_offset: args.offset + page.length < matches.length ? args.offset + page.length : null };
645
+ result = { tasks: page.map(({ id, text, state, tag }) => ({ id, title: taskTitle(text).slice(0, 1000), state, tag })), total: matches.length, next_offset: args.offset + page.length < matches.length ? args.offset + page.length : null };
645
646
  }
646
647
  else if (request.params.name === 'get_task') {
647
648
  const args = ExistingProposal.parse(request.params.arguments);
@@ -0,0 +1,4 @@
1
+ /** Display-only title: keep Markdown source intact for editing and previews. */
2
+ export function taskTitle(text) {
3
+ return text.split("\n", 1)[0].replace(/^ {0,3}#{1,6}[ \t]+/u, "");
4
+ }
package/index.ts CHANGED
@@ -11,6 +11,7 @@ import { FooterSettings, TaskMenu, withSettingsTab, type SettingsOption, type Se
11
11
  import { TaskPicker, taskState, taskTag } from "./task-picker.ts";
12
12
  import { bundledCLIVersion, detectCLI, setupCLI, setupHint, versionAtLeast, type CLIAction } from "./setup.ts";
13
13
  import { createPreviewBridge } from "./preview-bridge.ts";
14
+ import { taskTitle } from "./task-title.ts";
14
15
  import { renderSuggestion, renderSelectedTask } from "./task-suggestion.ts";
15
16
 
16
17
  import { proposalTitleOptions, proposalDescriptionOptions, proposalText } from "./proposal.ts";
@@ -52,7 +53,7 @@ const record = (value: unknown): value is Record<string, unknown> =>
52
53
  const text = (value: unknown): value is string =>
53
54
  typeof value === "string" && !/[\x00-\x08\x0b-\x1f\x7f-\x9f]/u.test(value);
54
55
  const clean = (value: string) => stripVTControlCharacters(value).replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/gu, "�");
55
- const firstLine = (task: Summary) => clean(task.text.split("\n", 1)[0]).replace(/\s+/gu, " ").trim();
56
+ const firstLine = (task: Summary) => clean(taskTitle(task.text)).replace(/\s+/gu, " ").trim();
56
57
  const title = (task: Summary) => truncateToWidth(`#${task.id} ${firstLine(task)}`, 60);
57
58
 
58
59
  function readConfig(): Record<string, unknown> {
package/mcp/core.ts CHANGED
@@ -14,6 +14,7 @@ import { stripVTControlCharacters } from 'node:util';
14
14
  import { defaultAgentGuidance, defaultFooterField, defaultHandoffPrompt, defaultNewSessionPrompt, effectiveFooterFields, parseAgentGuidance, parseFooterConfig, parseHandoffPrompt, parseNewSessionPrompt, readConfigDocument, readFooterDocument, saveFooterConfig } from '../footer-config.ts';
15
15
  import { proposalTitleOptions, proposalDescriptionOptions, proposalText } from '../proposal.ts';
16
16
  import type { FieldAutomation, FieldResult } from './field-events.ts';
17
+ import { taskTitle } from '../task-title.ts';
17
18
 
18
19
  const URI = 'pinote://ui';
19
20
  const MIME = 'application/vnd.mcp-native-ui+json';
@@ -175,7 +176,7 @@ function view(data: Snapshot, scoped = false, secondary = false, features: { foo
175
176
  const controls: Control[] = [];
176
177
  const action = (id: string, label: string, extra: Partial<Control> = {}) => controls.push({ id, label: label.slice(0, 2000), kind: 'action', ...extra });
177
178
  let title = 'Pinote · Tasks';
178
- let body = task ? `#${task.id} · ${task.tag || 'No tag'} · ${humanStatus(task.state)} · CURRENT\n${task.text.split('\n')[0]}` : state.proposal ? `Suggestion only; no task is selected for this conversation.\n${suggestion!.text.split('\n')[0]}` : 'Select a task or create one. No task is selected for this conversation.';
179
+ let body = task ? `#${task.id} · ${task.tag || 'No tag'} · ${humanStatus(task.state)} · CURRENT\n${taskTitle(task.text)}` : state.proposal ? `Suggestion only; no task is selected for this conversation.\n${taskTitle(suggestion!.text)}` : 'Select a task or create one. No task is selected for this conversation.';
179
180
  // Footer actions remain valid on every screen, including Settings.
180
181
  if (task) {
181
182
  action('preview', 'Preview full task', { description: 'Display only; never sent to the model' });
@@ -221,22 +222,22 @@ function view(data: Snapshot, scoped = false, secondary = false, features: { foo
221
222
  const matches = tasks.filter((t) => state.launch
222
223
  ? t.id === state.launch.task_id && t.updated_at === state.launch.updated_at
223
224
  : terms.every((term) => `${t.text} ${t.tag ?? ''} #${t.id}`.toLowerCase().includes(term)));
224
- for (const candidate of matches.slice(0, 200)) action(`select:${candidate.id}`, candidate.text.split('\n')[0], { description: `#${candidate.id} · ${candidate.tag || 'No tag'} · ${humanStatus(candidate.state)}${candidate.id === task?.id ? ' · CURRENT' : ''} · Select and start this task for this conversation` });
225
+ for (const candidate of matches.slice(0, 200)) action(`select:${candidate.id}`, taskTitle(candidate.text), { description: `#${candidate.id} · ${candidate.tag || 'No tag'} · ${humanStatus(candidate.state)}${candidate.id === task?.id ? ' · CURRENT' : ''} · Select and start this task for this conversation` });
225
226
  if (matches.length > 200) body += '\nShowing the first 200 matches; use Filter tasks to narrow the list.';
226
227
  }
227
228
  const footer: { text: string; openApp?: true; url?: string; action?: string; secondaryAction?: string; doubleAction?: string; style?: { italic?: boolean } & NonNullable<FieldResult['display']>; width?: number }[] = features.footerNativeActions
228
229
  ? [{ text: `${icons['open-app']} `, openApp: true, width: 3 }]
229
230
  : [{ text: `${icons.note} `, width: 3 }];
230
231
  if (features.footerNativeActions) {
231
- if (task) 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 });
232
- else if (state.proposal) footer.push({ text: `? ${suggestion!.tag ? `[${suggestion!.tag}] ` : ''}${suggestion!.text.split('\n')[0].slice(0, 2000)}`, action: 'proposal:preview', ...(features.footerDoubleActions ? { doubleAction: 'proposal:accept' } : {}), ...(features.footerStyles ? { style: { italic: true, color: 'accent' } as const } : {}), width: config.config.titleWidth });
232
+ if (task) footer.push({ text: `${task.tag ? `[${task.tag}] ` : ''}${taskTitle(task.text).slice(0, 2000)}`, action: 'preview', ...(features.footerDoubleActions ? { doubleAction: 'done' } : {}), width: config.config.titleWidth });
233
+ else if (state.proposal) footer.push({ text: `? ${suggestion!.tag ? `[${suggestion!.tag}] ` : ''}${taskTitle(suggestion!.text).slice(0, 2000)}`, action: 'proposal:preview', ...(features.footerDoubleActions ? { doubleAction: 'proposal:accept' } : {}), ...(features.footerStyles ? { style: { italic: true, color: 'accent' } as const } : {}), width: config.config.titleWidth });
233
234
  else footer.push({ text: 'Pinote · No task', width: config.config.titleWidth });
234
235
  } else if (task) {
235
236
  footer.push({ text: `${icons.check} `, action: 'done', width: 3 });
236
- footer.push({ text: `${task.tag ? `[${task.tag}] ` : ''}${task.text.split('\n')[0].slice(0, 2000)}`, action: 'preview', width: config.config.titleWidth });
237
+ footer.push({ text: `${task.tag ? `[${task.tag}] ` : ''}${taskTitle(task.text).slice(0, 2000)}`, action: 'preview', width: config.config.titleWidth });
237
238
  } else if (state.proposal) {
238
239
  footer.push({ text: `${icons.add} `, action: 'proposal:accept', ...(secondary ? { secondaryAction: 'proposal:dismiss' } : {}), width: 3 });
239
- footer.push({ text: `${suggestion!.tag ? `[${suggestion!.tag}] ` : ''}${suggestion!.text.split('\n')[0].slice(0, 2000)}`, action: 'proposal:preview', ...(features.footerStyles ? { style: { italic: true, color: 'accent' } as const } : {}), width: config.config.titleWidth });
240
+ footer.push({ text: `${suggestion!.tag ? `[${suggestion!.tag}] ` : ''}${taskTitle(suggestion!.text).slice(0, 2000)}`, action: 'proposal:preview', ...(features.footerStyles ? { style: { italic: true, color: 'accent' } as const } : {}), width: config.config.titleWidth });
240
241
  } else footer.push({ text: 'Pinote · no task', width: config.config.titleWidth });
241
242
  let fieldCount = 0;
242
243
  if (task) {
@@ -536,7 +537,7 @@ server.setRequestHandler(CallToolRequestSchema, (request, extra) => serial(async
536
537
  const tasks: Task[] = JSON.parse(await note(['list', '--json'], extra.signal));
537
538
  const matches = tasks.filter((task) => words.every((word) => `${task.text} ${task.tag ?? ''}`.toLowerCase().includes(word)));
538
539
  const page = matches.slice(args.offset, args.offset + args.limit);
539
- result = { tasks: page.map(({ id, text, state, tag }) => ({ id, title: text.split('\n')[0].slice(0, 1000), state, tag })), total: matches.length, next_offset: args.offset + page.length < matches.length ? args.offset + page.length : null };
540
+ result = { tasks: page.map(({ id, text, state, tag }) => ({ id, title: taskTitle(text).slice(0, 1000), state, tag })), total: matches.length, next_offset: args.offset + page.length < matches.length ? args.offset + page.length : null };
540
541
  } else if (request.params.name === 'get_task') {
541
542
  const args = ExistingProposal.parse(request.params.arguments);
542
543
  result = JSON.parse(await note(['agent', 'get', String(args.task_id)], extra.signal));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-note",
3
- "version": "0.39.3",
3
+ "version": "0.39.4",
4
4
  "description": "Persistent pinote tasks, Markdown handoffs, and task status for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -42,6 +42,7 @@
42
42
  "footer-settings.ts",
43
43
  "task-picker.ts",
44
44
  "task-suggestion.ts",
45
+ "task-title.ts",
45
46
  "proposal.ts",
46
47
  "setup.ts",
47
48
  "preview-bridge.ts",
@@ -1,4 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
+ import { taskTitle } from "./task-title.ts";
2
3
  import { stripVTControlCharacters } from "node:util";
3
4
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
4
5
  import type { Theme } from "@earendil-works/pi-coding-agent";
@@ -20,7 +21,7 @@ function renderTask(task: Task, width: number, theme: Colors, prefix: string, pr
20
21
  if (remaining < 4) return prefix.trimEnd();
21
22
  const clean = (value: string) => stripVTControlCharacters(value).replace(/[\x00-\x1f\x7f-\x9f]/gu, " ").replace(/\s+/gu, " ").trim();
22
23
  const tag = `[${clean(task.tag ?? "Untagged")}]`;
23
- const title = clean(task.text.split("\n", 1)[0]);
24
+ const title = clean(taskTitle(task.text));
24
25
  const tagWidth = visibleWidth(tag) + 1;
25
26
  const label = remaining >= tagWidth + Math.min(12, visibleWidth(title))
26
27
  ? `${theme.fg("muted", tag)} ${theme.fg("text", truncateToWidth(title, remaining - tagWidth, "..."))}`
package/task-title.ts ADDED
@@ -0,0 +1,4 @@
1
+ /** Display-only title: keep Markdown source intact for editing and previews. */
2
+ export function taskTitle(text: string): string {
3
+ return text.split("\n", 1)[0].replace(/^ {0,3}#{1,6}[ \t]+/u, "");
4
+ }