From 0bef3b9fbe035db325b7b025cb8de0a070780a6c Mon Sep 17 00:00:00 2001 From: lyuksovannyy Date: Mon, 12 Oct 2026 10:35:43 +0000 Subject: [PATCH] Session titles: strip a leading machine-name prefix - remote_task strips the device name from the title it creates, and tells the model not to include it - session lists (Devices tab, sidebar, remote_devices) strip it from existing titles - tests for stripDevicePrefix --- index.js | 17 ++++++++++++++--- test/title.test.mjs | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 test/title.test.mjs diff --git a/index.js b/index.js index ed926ab..c4e914a 100644 --- a/index.js +++ b/index.js @@ -56,6 +56,17 @@ const MUX_PATH = '/api/remote.mux' const OPS = new Set(['exec', 'readFile', 'writeFile', 'listDir', 'task', 'taskStatus', 'taskCancel', 'inspectSession']) /** Pseudo-secret name: opting into it ships the master's forge (git) logins. */ const FORGE_SECRET = 'FORGE_CREDENTIALS' +/** Session titles must not carry the machine name ("ai-pc: fix tests" → "fix tests"): the device is shown separately. */ +const stripDevicePrefix = (title, names) => { + let t = String(title ?? '') + for (const n of names.filter(Boolean)) { + const esc = String(n).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + // "[name] x" / "(name) x" (brackets close the prefix) or "name: x" / "name - x" / "name | x" + const re = new RegExp(`^\\s*(?:[\\[(]${esc}[\\])]\\s*[:·|>–—-]?|${esc}\\s*[:·|>–—-])\\s*`, 'i') + t = t.replace(re, '') + } + return t.trim() +} function sendJson(res, status, payload) { res.statusCode = status @@ -258,7 +269,7 @@ export function apply(ctx, config = {}) { updatedAt: s.updatedAt, running: s.running, cwd: s.cwd, - title: s.projections?.values?.title ?? null, + title: stripDevicePrefix(s.projections?.values?.title, [self.id, self.name]) || (s.projections?.values?.title ?? null), })), workspaces, } @@ -1165,7 +1176,7 @@ document.getElementById('go').onclick=()=>{ prompt: S('string', 'run: the full task for the remote agent.'), sessionId: S('string', 'Continue / poll / cancel this session on the device.'), cwd: CWD, - title: S('string', 'run, new session: session title.'), + title: S('string', 'run, new session: session title. Describe the task only; do not include the device name (it is shown next to the session already).'), permission: S('string', 'run: permission preset for the remote session, e.g. "danger-full-access" when the job must run tools outside the workspace sandbox (test suites that spawn processes, git credential writes). Default: the device\'s default preset.'), wait: S('boolean', 'run: wait for completion (default true). false = start and return the sessionId.'), timeoutMs: S('integer', 'run: how long to wait (default 1200000 = 20 min, max 2 h). On timeout the task keeps running; poll with action=status.'), @@ -1179,7 +1190,7 @@ document.getElementById('go').onclick=()=>{ if (action === 'cancel') return { device, ...(await callDevice(device, 'taskCancel', { sessionId: args.sessionId }, 15000)) } const t = Math.min(Math.max(Number(args.timeoutMs) || 1200000, 10000), 2 * 60 * 60 * 1000) const r = await callDevice(device, 'task', { - prompt: args.prompt, sessionId: args.sessionId, cwd: args.cwd, title: args.title, permission: args.permission, + prompt: args.prompt, sessionId: args.sessionId, cwd: args.cwd, title: stripDevicePrefix(args.title, [device, args.device, devicesView().find((d) => d.id === device)?.name]) || undefined, permission: args.permission, wait: args.wait !== false, timeoutMs: t, }, t + 30000) return { device, ...r, url: device === self.id ? `#/session/${r.sessionId}` : `/r/${device}/#/session/${r.sessionId}` } diff --git a/test/title.test.mjs b/test/title.test.mjs new file mode 100644 index 0000000..4a798b0 --- /dev/null +++ b/test/title.test.mjs @@ -0,0 +1,34 @@ +// Session titles must not start with the machine name. stripDevicePrefix is private to +// index.js, so the test extracts its source and evaluates it. +import test from 'node:test' +import assert from 'node:assert/strict' +import fs from 'node:fs' + +const src = fs.readFileSync(new URL('../index.js', import.meta.url), 'utf8') +const m = src.match(/const stripDevicePrefix = [\s\S]*?\n}\n/) +assert.ok(m, 'stripDevicePrefix not found in index.js') +const stripDevicePrefix = new Function(`${m[0]}; return stripDevicePrefix`)() + +test('removes a leading machine name with common separators', () => { + const names = ['ai-pc', 'AI PC'] + assert.equal(stripDevicePrefix('ai-pc: git pull + unit tests', names), 'git pull + unit tests') + assert.equal(stripDevicePrefix('AI-PC - run tests', names), 'run tests') + assert.equal(stripDevicePrefix('[ai-pc] run tests', names), 'run tests') + assert.equal(stripDevicePrefix('(ai-pc) · run tests', names), 'run tests') + assert.equal(stripDevicePrefix('ai-pc | run tests', names), 'run tests') + assert.equal(stripDevicePrefix('ai-pc> run tests', names), 'run tests') +}) + +test('keeps titles that merely mention the machine, and empty titles', () => { + const names = ['ai-pc'] + assert.equal(stripDevicePrefix('Test and fix ai-pc deployment', names), 'Test and fix ai-pc deployment') + assert.equal(stripDevicePrefix('ai-pc deployment notes', names), 'ai-pc deployment notes') + assert.equal(stripDevicePrefix('Clone and test ai-pc repository remotely', names), 'Clone and test ai-pc repository remotely') + assert.equal(stripDevicePrefix('', names), '') + assert.equal(stripDevicePrefix(null, names), '') +}) + +test('strips every known name, not only the first', () => { + assert.equal(stripDevicePrefix('Pelican-Node: backup', ['pelican-node', 'Pelican-Node']), 'backup') + assert.equal(stripDevicePrefix('mac: backup', ['mac', 'my-mac']), 'backup') +})