pi-note 0.39.2 → 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 +15 -5
- package/dist-mcp/mcp/core.js +8 -7
- package/dist-mcp/task-title.js +4 -0
- package/index.ts +2 -1
- package/mcp/core.ts +8 -7
- package/package.json +2 -1
- package/remote/ui.ts +6 -5
- package/task-suggestion.ts +2 -1
- package/task-title.ts +4 -0
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.
|
|
@@ -58,9 +62,10 @@ confirmation. **Clear selection** leaves its progress unchanged.
|
|
|
58
62
|
|
|
59
63
|
**Preview** opens a scrollable, read-only document. **Insert task prompt…** reviews
|
|
60
64
|
and inserts into an empty editor only; it never submits or replaces a draft.
|
|
61
|
-
Creation, suggestion acceptance and shared changes require complete review,
|
|
62
|
-
a separate Cancel-first confirmation. Menu completion also asks explicitly
|
|
63
|
-
|
|
65
|
+
Creation, menu suggestion acceptance and shared changes require complete review,
|
|
66
|
+
then a separate Cancel-first confirmation. Menu completion also asks explicitly.
|
|
67
|
+
Footer double-click directly approves suggestion acceptance or completion;
|
|
68
|
+
completion requires idle Pi. Failed or uncertain
|
|
64
69
|
writes offer refresh/verification, never automatic replay.
|
|
65
70
|
|
|
66
71
|
**Settings** groups prompts/guidance, task-offer policy, footer layout and field
|
|
@@ -73,7 +78,8 @@ Menus use Pi's native inline components, themes and configured selection keys.
|
|
|
73
78
|
Small terminals show compact menus or ask you to enlarge the window before acting.
|
|
74
79
|
Fullscreen has a clickable footer: **▤** opens Pinote; single-click a task title
|
|
75
80
|
to preview, or double-click to complete immediately without another confirmation
|
|
76
|
-
(requires idle Pi).
|
|
81
|
+
(requires idle Pi). Single-click a suggestion title to preview it; double-click
|
|
82
|
+
to accept, start and select it immediately without another confirmation.
|
|
77
83
|
Regular mode shows status; `/pinote` provides all actions. No Kitty action handler
|
|
78
84
|
is needed.
|
|
79
85
|
|
|
@@ -215,10 +221,14 @@ See [setup, script contract and safeguards](mcp/FIELD_EVENTS.md). Disabled by de
|
|
|
215
221
|
|
|
216
222
|
## Migration
|
|
217
223
|
|
|
224
|
+
Since pi-note 0.39.3, fullscreen suggestion-title double-click accepts immediately;
|
|
225
|
+
single-click still previews and menu acceptance still requires review/confirmation.
|
|
226
|
+
Update both client and service, then `/reload` Pi.
|
|
227
|
+
|
|
218
228
|
Since pi-note 0.39.2, fullscreen task-title double-click completes without another
|
|
219
229
|
confirmation; single-click still previews and menu completion still confirms.
|
|
220
230
|
Update the client and `/reload` Pi. The service must support footer double-click
|
|
221
|
-
(pi-note 0.39.1+).
|
|
231
|
+
(pi-note 0.39.1+).
|
|
222
232
|
|
|
223
233
|
Since pi-note 0.38.0, the default HTTP extension uses task-first navigation and
|
|
224
234
|
explicit completion/suggestion approval. Update the client and service together,
|
package/dist-mcp/mcp/core.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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);
|
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
|
|
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
|
|
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
|
|
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
|
|
232
|
-
else if (state.proposal) footer.push({ text: `? ${suggestion!.tag ? `[${suggestion!.tag}] ` : ''}${suggestion!.text
|
|
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
|
|
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
|
|
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
|
|
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
|
+
"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",
|
package/remote/ui.ts
CHANGED
|
@@ -43,8 +43,9 @@ export class RemoteUI {
|
|
|
43
43
|
if (this.opening || this.acting) return false;
|
|
44
44
|
if (item.openApp) { await this.open(ctx); return true; }
|
|
45
45
|
const control = view.controls.find((c) => c.id === action && c.kind === 'action');
|
|
46
|
-
//
|
|
47
|
-
|
|
46
|
+
// Deliberate footer double-click approves completion or suggestion acceptance.
|
|
47
|
+
const approved = (action === 'done' || action === 'proposal:accept') && item.doubleAction === action;
|
|
48
|
+
return control ? this.dispatch(ctx, view, control, undefined, approved) : false;
|
|
48
49
|
}, this.repaint);
|
|
49
50
|
}, { placement: 'belowEditor' });
|
|
50
51
|
this.render();
|
|
@@ -66,7 +67,7 @@ export class RemoteUI {
|
|
|
66
67
|
}) : 'Pinote · unavailable (/pinote to reconnect)');
|
|
67
68
|
this.repaint();
|
|
68
69
|
}
|
|
69
|
-
async dispatch(ctx: ExtensionContext, view: NativeView, control: NativeControl, value?: NativeControl['value'],
|
|
70
|
+
async dispatch(ctx: ExtensionContext, view: NativeView, control: NativeControl, value?: NativeControl['value'], footerConfirmed = false): Promise<boolean> {
|
|
70
71
|
if (ctx.mode !== 'tui') throw new Error('Pinote UI actions require TUI mode');
|
|
71
72
|
if (this.acting) return false;
|
|
72
73
|
if (control.id === 'done' && !ctx.isIdle()) {
|
|
@@ -76,14 +77,14 @@ export class RemoteUI {
|
|
|
76
77
|
const current = this.session.current(), signal = this.session.signal;
|
|
77
78
|
try {
|
|
78
79
|
if (typeof value === 'string') editorText(value);
|
|
79
|
-
if (control.id === 'done' && !
|
|
80
|
+
if (control.id === 'done' && !footerConfirmed) {
|
|
80
81
|
const exits = typeof view.state.selectedId === 'number' && this.exitOnComplete(view.state.selectedId);
|
|
81
82
|
const accept = exits ? 'Complete task and exit' : 'Complete task';
|
|
82
83
|
const summary = truncateToWidth(line(view.body ?? ''), 100);
|
|
83
84
|
if (await ctx.ui.select(`Pinote / ${accept} #${view.state.selectedId}\n${summary}\n${exits ? 'Closes this dedicated Pi session.' : 'Clears selection; keeps this conversation and editor.'}`,
|
|
84
85
|
['Cancel', accept], { signal }) !== accept) return false;
|
|
85
86
|
}
|
|
86
|
-
if (control.confirm) {
|
|
87
|
+
if (control.confirm && !(footerConfirmed && control.id === 'proposal:accept')) {
|
|
87
88
|
const details = control.kind === 'action' ? control.confirm : `${control.confirm}\n\n${control.id === 'add' || control.id === 'add-field' ? '' : `Before:\n${disclosure(control.value)}\n\n`}After:\n${disclosure(value)}`;
|
|
88
89
|
const accept = control.id === 'add' ? 'Create, start and select' : control.id === 'proposal:accept' ? 'Accept suggestion' :
|
|
89
90
|
control.id === 'remove-field' ? 'Remove display field' : control.id === 'add-field' ? 'Add field' : 'Save change';
|
package/task-suggestion.ts
CHANGED
|
@@ -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
|
|
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