termdock 1.4.69 → 1.4.70
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/.desktop-dist/main.js +254 -49
- package/README.md +6 -0
- package/dist/client/assets/{ModelPreview-Bk8GM7G-.js → ModelPreview-Cs14IQuQ.js} +1 -1
- package/dist/client/assets/{abnfDiagram-VRR7QNED-BYiQXGYk.js → abnfDiagram-VRR7QNED-DuDfdv-6.js} +1 -1
- package/dist/client/assets/{arc-jlT_jmoZ.js → arc-CSYzGV_2.js} +1 -1
- package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-D1qZ875Q.js → architectureDiagram-ZJ3FMSHR-YiZ0HXNi.js} +1 -1
- package/dist/client/assets/{blockDiagram-677ZJIJ3-qkgn2Ysc.js → blockDiagram-677ZJIJ3-IaQMzmYv.js} +1 -1
- package/dist/client/assets/{c4Diagram-LMCZKHZV-JgcH49Pf.js → c4Diagram-LMCZKHZV-BwyuIPRL.js} +1 -1
- package/dist/client/assets/channel-BotxtiNE.js +1 -0
- package/dist/client/assets/{chunk-2Q5K7J3B-E-3iflEy.js → chunk-2Q5K7J3B-BtHQYLBT.js} +1 -1
- package/dist/client/assets/{chunk-32BRIVSS-mM01y3kF.js → chunk-32BRIVSS-CXrK07sW.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-GO8Muo5l.js → chunk-5VM5RSS4-BfBshn11.js} +1 -1
- package/dist/client/assets/{chunk-EX3LRPZG-BUSDluH0.js → chunk-EX3LRPZG-BJbilNaf.js} +1 -1
- package/dist/client/assets/{chunk-JWPE2WC7-obSL6EzJ.js → chunk-JWPE2WC7-wgcmSRlL.js} +1 -1
- package/dist/client/assets/{chunk-MOJQB5TN-zEktbtAG.js → chunk-MOJQB5TN-8VZVUeDI.js} +1 -1
- package/dist/client/assets/{chunk-RYQCIY6F-kH7H8XgQ.js → chunk-RYQCIY6F-B900YBFa.js} +1 -1
- package/dist/client/assets/{chunk-V7JOEXUC-BqDFpHKN.js → chunk-V7JOEXUC-BK4_RLz-.js} +1 -1
- package/dist/client/assets/{chunk-VR4S4FIN-BGZ7kCIG.js → chunk-VR4S4FIN-Kj5_ycjB.js} +1 -1
- package/dist/client/assets/{chunk-XXDRQBXY-D-TatWXe.js → chunk-XXDRQBXY-BolvPLRz.js} +1 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-CLWlbTSC.js +1 -0
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-CLWlbTSC.js +1 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-CfWyrViz.js → cose-bilkent-JH36ORCC-9_9_SLqv.js} +1 -1
- package/dist/client/assets/{cynefin-VYW2F7L2-BFdZ3FcE.js → cynefin-VYW2F7L2-yEpoi3vG.js} +1 -1
- package/dist/client/assets/{cynefinDiagram-TSTJHNR4-iAfCVoFU.js → cynefinDiagram-TSTJHNR4-Dy-gvup_.js} +1 -1
- package/dist/client/assets/{dagre-VKFMJZFB-CZhh0hrr.js → dagre-VKFMJZFB-CNjJ5pRK.js} +1 -1
- package/dist/client/assets/{diagram-FQU43EPY-BqUK2OmV.js → diagram-FQU43EPY-uZ73Rj3T.js} +1 -1
- package/dist/client/assets/{diagram-G47NLZAW-vC0pSQYC.js → diagram-G47NLZAW-Cxt7TYZT.js} +1 -1
- package/dist/client/assets/{diagram-NH7WQ7WH-Dqc5oxed.js → diagram-NH7WQ7WH-B4TKinuZ.js} +1 -1
- package/dist/client/assets/{diagram-OA4YK3LP-DIIXckFE.js → diagram-OA4YK3LP-CGSuenHU.js} +1 -1
- package/dist/client/assets/{diagram-WEI45ONY-DR9_cFeh.js → diagram-WEI45ONY-DqqzDaUZ.js} +1 -1
- package/dist/client/assets/{ebnfDiagram-CCIWWBDH-21OO0OXC.js → ebnfDiagram-CCIWWBDH-B2SgXL5w.js} +1 -1
- package/dist/client/assets/{erDiagram-Q63AITRT-YKnVG3az.js → erDiagram-Q63AITRT-D-ibkr8y.js} +1 -1
- package/dist/client/assets/{flowDiagram-23GEKE2U-Cm7TGErF.js → flowDiagram-23GEKE2U-BPeyMqr-.js} +1 -1
- package/dist/client/assets/{ganttDiagram-NO4QXBWP-D_76Effz.js → ganttDiagram-NO4QXBWP-DcEqEzzu.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-CC1ysmVD.js → gitGraphDiagram-IHSO6WYX-DTZ6UCQY.js} +1 -1
- package/dist/client/assets/index-DhfIiBLW.css +1 -0
- package/dist/client/assets/index-ivnGnbVg.js +451 -0
- package/dist/client/assets/{infoDiagram-FWYZ7A6U-BbgpmhBz.js → infoDiagram-FWYZ7A6U-BHrzVXeX.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-C94bY_je.js → ishikawaDiagram-FXEZZL3T-CwuoQJku.js} +1 -1
- package/dist/client/assets/{journeyDiagram-5HDEW3XC-BQ0iFRxs.js → journeyDiagram-5HDEW3XC-ZEcy0rDZ.js} +1 -1
- package/dist/client/assets/{kanban-definition-HUTT4EX6-BtAexXPo.js → kanban-definition-HUTT4EX6-BDBawNbr.js} +1 -1
- package/dist/client/assets/{linear-C2HnIdOK.js → linear-smYQWzDV.js} +1 -1
- package/dist/client/assets/{mermaid.core-BbG4sJBF.js → mermaid.core-eJs7Ouvw.js} +4 -4
- package/dist/client/assets/{mindmap-definition-LN4V7U3C-B-uvViws.js → mindmap-definition-LN4V7U3C-BK1IZ9zg.js} +1 -1
- package/dist/client/assets/{pegDiagram-2B236MQR-DHYcR6tE.js → pegDiagram-2B236MQR-B3-mVNgX.js} +1 -1
- package/dist/client/assets/{pieDiagram-ENE6RG2P-DEE9cD6M.js → pieDiagram-ENE6RG2P-Bkeeyx5l.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-DMQP5jG3.js → quadrantDiagram-ABIIQ3AL-DouIklEO.js} +1 -1
- package/dist/client/assets/{railroadDiagram-RFXS5EU6-B-4WDTwc.js → railroadDiagram-RFXS5EU6-BB_jXQd2.js} +1 -1
- package/dist/client/assets/{requirementDiagram-TGXJPOKE-C2xw_q-R.js → requirementDiagram-TGXJPOKE-u56IHfiR.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-HTMAVEWB-wq0BPqhc.js → sankeyDiagram-HTMAVEWB-eDi2cs5E.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-BqpM0A9d.js → sequenceDiagram-DBY2YBRQ-DnAA2xYa.js} +1 -1
- package/dist/client/assets/{sizeCapture-X5ZJPWSS-Ba2xxyp_.js → sizeCapture-X5ZJPWSS-BogPui79.js} +1 -1
- package/dist/client/assets/{stateDiagram-2N3HPSRC-agPx5qAN.js → stateDiagram-2N3HPSRC--EeXDy2M.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-Dc-3HKTs.js +1 -0
- package/dist/client/assets/{swimlanes-5IMT3BWC-Cy6Lzj_L.js → swimlanes-5IMT3BWC-DZAw7-kY.js} +2 -2
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-CI1Wlr0V.js +8 -0
- package/dist/client/assets/{timeline-definition-FHXFAJF6-LBrVfbdp.js → timeline-definition-FHXFAJF6-DkcnpB-Y.js} +1 -1
- package/dist/client/assets/{vennDiagram-L72KCM5P-BiYIV7mX.js → vennDiagram-L72KCM5P-BStcDGEf.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-EHGQE667-Q1QU9gG3.js → wardleyDiagram-EHGQE667-Cj4CQn1M.js} +1 -1
- package/dist/client/assets/{xychartDiagram-FW5EYKEG-CzYpxk4l.js → xychartDiagram-FW5EYKEG-_uBCjai8.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/sw.js +1 -1
- package/dist/server/agent/autoTitle.js +118 -0
- package/dist/server/agent/installers.js +11 -10
- package/dist/server/agent/plugins.js +90 -4
- package/dist/server/agent/registry.js +1 -0
- package/dist/server/agent/session.js +18 -2
- package/dist/server/agent/titleNamerCatalog.js +203 -0
- package/dist/server/agentHook.js +2 -1
- package/dist/server/cli.js +218 -30
- package/dist/server/routes/filesystem.js +11 -8
- package/dist/server/routes/terminal.js +351 -10
- package/dist/server/utils/autoUpdate.js +175 -0
- package/dist/server/utils/npmUpdate.js +2 -1
- package/dist/server/utils/settings.js +44 -0
- package/package.json +1 -1
- package/runtime-manifest.json +2 -2
- package/dist/client/assets/channel-C2i7H7bH.js +0 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-BlGG6XFj.js +0 -1
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-BlGG6XFj.js +0 -1
- package/dist/client/assets/index-ChhyERKX.css +0 -1
- package/dist/client/assets/index-r-PkH7SB.js +0 -442
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-beoQXpWJ.js +0 -1
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-BxL1xNhC.js +0 -8
|
@@ -6,7 +6,7 @@ import { createRequire } from 'module';
|
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import QRCode from 'qrcode';
|
|
8
8
|
import { execFile, spawn } from 'child_process';
|
|
9
|
-
import { randomUUID } from 'crypto';
|
|
9
|
+
import { createHash, randomUUID } from 'crypto';
|
|
10
10
|
import { promisify } from 'util';
|
|
11
11
|
import { caffeinateManager } from '../utils/caffeinate.js';
|
|
12
12
|
import { gitStatusCache } from '../utils/gitStatus.js';
|
|
@@ -14,19 +14,22 @@ import { getPtyHostManager } from '../ptyhost/manager.js';
|
|
|
14
14
|
import { pathValidator } from '../utils/pathValidator.js';
|
|
15
15
|
import { TERMINAL, TMUX } from '../config.js';
|
|
16
16
|
import { localAccessManager } from '../utils/localAccess.js';
|
|
17
|
-
import { normalizeLocalAccessName, getLocaleSetting, setLocaleSetting, getContextDraftHeightSetting, setContextDraftHeightSetting, } from '../utils/settings.js';
|
|
17
|
+
import { normalizeLocalAccessName, getLocaleSetting, setLocaleSetting, getContextDraftHeightSetting, setContextDraftHeightSetting, getAutoRenameAgentsSetting, setAutoRenameAgentsSetting, getAutoRenameNamerSetting, setAutoRenameNamerSetting, getAutoRenameModelsSetting, setAutoRenameModelsSetting, } from '../utils/settings.js';
|
|
18
18
|
import { loadContextDraft, saveContextDraft } from '../utils/contextDraft.js';
|
|
19
19
|
import { getOnboardingServerUrl } from '../onboardingServer.js';
|
|
20
20
|
import { getFocusSequence, removeClientFocusState, scanFocusTrackingMode, setClientFocusState, } from '../utils/tmuxFocus.js';
|
|
21
21
|
import { extractProgramLabelFromArgs, normalizeProgramName, normalizeTmuxMetadataProgram, selectTmuxForegroundProgram, tmuxMetadataChanged, } from '../utils/tmuxProgramDetection.js';
|
|
22
|
-
import { agentBySlug, buildResumeCommand, detectAgentFromCommand, } from '../agent/registry.js';
|
|
22
|
+
import { agentBySlug, buildResumeCommand, detectAgentFromCommand, listAgents, } from '../agent/registry.js';
|
|
23
23
|
import { applyAgentEvent, defaultAgentSessionState, parseAgentEvent, } from '../agent/session.js';
|
|
24
24
|
import { listAllHookAgents, refreshStaleHooksAtLaunch, installHooksForSlug, uninstallHooksForSlug, } from '../agent/installers.js';
|
|
25
|
-
import { loadPlugins, savePlugin, removePlugin, readPluginIcon, } from '../agent/plugins.js';
|
|
26
|
-
import { registerPluginAgents } from '../agent/registry.js';
|
|
25
|
+
import { loadPlugins, savePlugin, removePlugin, readPluginIcon, validateManifest, } from '../agent/plugins.js';
|
|
26
|
+
import { clearPluginAgents, registerPluginAgents } from '../agent/registry.js';
|
|
27
27
|
import { notifyAgentTransition, notifyTerminalExit } from '../notifications/pushService.js';
|
|
28
28
|
import { setClientViewingSession } from '../notifications/pushViewers.js';
|
|
29
29
|
import { getQuotaStatus, refreshQuota, startQuotaManager, } from '../quota/QuotaManager.js';
|
|
30
|
+
import { TermdockAutoUpdateManager } from '../utils/autoUpdate.js';
|
|
31
|
+
import { cleanTerminalContext, generateAgentTitle, isNewAgentSessionId, shouldReplaceAutoTitle } from '../agent/autoTitle.js';
|
|
32
|
+
import { getTitleNamerCatalog } from '../agent/titleNamerCatalog.js';
|
|
30
33
|
const router = express.Router();
|
|
31
34
|
const execFileAsync = promisify(execFile);
|
|
32
35
|
const TERMDOCK_DIR = `${os.homedir()}/.termdock`;
|
|
@@ -193,6 +196,67 @@ function sendClientStatePayload(payload) {
|
|
|
193
196
|
function broadcastControlEvent(payload) {
|
|
194
197
|
sendClientStatePayload(JSON.stringify(payload));
|
|
195
198
|
}
|
|
199
|
+
let serverRestartScheduled = false;
|
|
200
|
+
function requestServerRestartAfterUpdate() {
|
|
201
|
+
if (serverRestartScheduled)
|
|
202
|
+
return;
|
|
203
|
+
serverRestartScheduled = true;
|
|
204
|
+
// The bridge is detached before this process exits. It waits for the old
|
|
205
|
+
// PID to release the port, then resolves `termdock` from PATH again so an
|
|
206
|
+
// npm-replaced global binary is used instead of the old loaded file.
|
|
207
|
+
const bridgeSource = String.raw `
|
|
208
|
+
const fs = require('node:fs');
|
|
209
|
+
const { spawn } = require('node:child_process');
|
|
210
|
+
const payload = JSON.parse(process.argv[1]);
|
|
211
|
+
const alive = () => { try { process.kill(payload.parentPid, 0); return true; } catch { return false; } };
|
|
212
|
+
const launch = () => {
|
|
213
|
+
let fd;
|
|
214
|
+
try { fd = fs.openSync(payload.logFile, 'a'); } catch { fd = 'ignore'; }
|
|
215
|
+
const command = process.platform === 'win32' ? 'termdock.cmd' : 'termdock';
|
|
216
|
+
const child = spawn(command, payload.args, {
|
|
217
|
+
detached: true,
|
|
218
|
+
stdio: ['ignore', fd, fd],
|
|
219
|
+
env: process.env,
|
|
220
|
+
});
|
|
221
|
+
child.unref();
|
|
222
|
+
if (typeof fd === 'number') fs.closeSync(fd);
|
|
223
|
+
};
|
|
224
|
+
const deadline = Date.now() + 15000;
|
|
225
|
+
const timer = setInterval(() => {
|
|
226
|
+
if (alive() && Date.now() < deadline) return;
|
|
227
|
+
clearInterval(timer);
|
|
228
|
+
setTimeout(launch, 250);
|
|
229
|
+
}, 100);
|
|
230
|
+
`;
|
|
231
|
+
const payload = JSON.stringify({
|
|
232
|
+
parentPid: process.pid,
|
|
233
|
+
args: process.argv.slice(2),
|
|
234
|
+
logFile: path.join(TERMDOCK_DIR, 'server.log'),
|
|
235
|
+
});
|
|
236
|
+
const bridge = spawn(process.execPath, ['-e', bridgeSource, payload], {
|
|
237
|
+
detached: true,
|
|
238
|
+
stdio: 'ignore',
|
|
239
|
+
env: process.env,
|
|
240
|
+
});
|
|
241
|
+
bridge.unref();
|
|
242
|
+
const exitTimer = setTimeout(() => {
|
|
243
|
+
process.kill(process.pid, 'SIGTERM');
|
|
244
|
+
}, 750);
|
|
245
|
+
exitTimer.unref?.();
|
|
246
|
+
}
|
|
247
|
+
const npmAutoUpdateManager = new TermdockAutoUpdateManager({
|
|
248
|
+
currentVersion: TERMDOCK_VERSION,
|
|
249
|
+
stateFilePath: path.join(TERMDOCK_DIR, 'update-state.json'),
|
|
250
|
+
broadcast: (state) => broadcastControlEvent({ type: 'update-state', state }),
|
|
251
|
+
requestRestart: requestServerRestartAfterUpdate,
|
|
252
|
+
log: (message) => console.warn(message),
|
|
253
|
+
});
|
|
254
|
+
// Packaged desktop builds already have a signed app/runtime updater. The npm
|
|
255
|
+
// updater only owns standalone CLI services, otherwise both mechanisms could
|
|
256
|
+
// race to replace the runtime.
|
|
257
|
+
if (process.env.TERMDOCK_DESKTOP !== '1') {
|
|
258
|
+
npmAutoUpdateManager.start();
|
|
259
|
+
}
|
|
196
260
|
async function flushClientStateBroadcast() {
|
|
197
261
|
if (broadcastClientStateInFlight) {
|
|
198
262
|
broadcastClientStateNeedsRerun = true;
|
|
@@ -643,6 +707,12 @@ function normalizePersistedClientSession(input) {
|
|
|
643
707
|
sessionId: candidate.sessionId,
|
|
644
708
|
name: candidate.name,
|
|
645
709
|
customName: candidate.customName === true ? true : undefined,
|
|
710
|
+
autoTitle: candidate.autoTitle
|
|
711
|
+
&& typeof candidate.autoTitle.agentSlug === 'string'
|
|
712
|
+
&& typeof candidate.autoTitle.contentHash === 'string'
|
|
713
|
+
&& typeof candidate.autoTitle.updatedAt === 'number'
|
|
714
|
+
? candidate.autoTitle
|
|
715
|
+
: null,
|
|
646
716
|
backendSessionId: typeof candidate.backendSessionId === 'string' && candidate.backendSessionId.trim().length > 0
|
|
647
717
|
? candidate.backendSessionId
|
|
648
718
|
: null,
|
|
@@ -1301,7 +1371,10 @@ async function configureTmuxWheelBindings() {
|
|
|
1301
1371
|
]);
|
|
1302
1372
|
await runTmux([
|
|
1303
1373
|
'bind-key', '-n', 'MouseDrag1Pane',
|
|
1304
|
-
|
|
1374
|
+
// xterm reports button-motion in character cells. A one-cell movement is
|
|
1375
|
+
// common mouse jitter during an ordinary click, so require two cells before
|
|
1376
|
+
// treating the gesture as an intentional text-selection drag.
|
|
1377
|
+
"if-shell -F '#{||:#{pane_in_mode},#{mouse_any_flag}}' 'send-keys -M' 'if-shell -F \"#{||:#{||:#{e|>=:#{e|-:#{mouse_x},#{@termdock-mouse-down-x}},2},#{e|>=:#{e|-:#{@termdock-mouse-down-x},#{mouse_x}},2}},#{||:#{e|>=:#{e|-:#{mouse_y},#{@termdock-mouse-down-y}},2},#{e|>=:#{e|-:#{@termdock-mouse-down-y},#{mouse_y}},2}}}\" \"copy-mode -M\"'",
|
|
1305
1378
|
]);
|
|
1306
1379
|
await runTmux([
|
|
1307
1380
|
'bind-key', '-n', 'DoubleClick1Pane',
|
|
@@ -2054,6 +2127,98 @@ function detectSessionAgent(session) {
|
|
|
2054
2127
|
}
|
|
2055
2128
|
return null;
|
|
2056
2129
|
}
|
|
2130
|
+
const AUTO_TITLE_CONTEXT_LIMIT = 24_000;
|
|
2131
|
+
const AUTO_TITLE_REEVALUATE_MS = 60 * 60_000;
|
|
2132
|
+
const autoTitlePending = new Set();
|
|
2133
|
+
function appendAutoTitleContext(session, data) {
|
|
2134
|
+
session.autoTitleContext = (session.autoTitleContext + data).slice(-AUTO_TITLE_CONTEXT_LIMIT);
|
|
2135
|
+
}
|
|
2136
|
+
function clearAutoTitleForNewAgentSession(sessionId, session) {
|
|
2137
|
+
const record = globalSessionState.sessions.find((item) => item.backendSessionId === sessionId);
|
|
2138
|
+
if (!record?.autoTitle)
|
|
2139
|
+
return;
|
|
2140
|
+
const next = { ...record, customName: undefined, autoTitle: null };
|
|
2141
|
+
upsertGlobalSessionRecord(next);
|
|
2142
|
+
session.autoTitleContext = '';
|
|
2143
|
+
if (next.mode === 'tmux' && next.tmuxSessionName) {
|
|
2144
|
+
void unsetTmuxOption(next.tmuxSessionName, '@termdock-friendly-name');
|
|
2145
|
+
}
|
|
2146
|
+
schedulePersistGlobalState();
|
|
2147
|
+
broadcastClientState();
|
|
2148
|
+
console.log(`[auto-title] cleared stale title for new agent session in ${sessionId}`);
|
|
2149
|
+
}
|
|
2150
|
+
async function maybeAutoRenameSession(sessionId, agent, rawContext) {
|
|
2151
|
+
if (autoTitlePending.has(sessionId))
|
|
2152
|
+
return;
|
|
2153
|
+
if (!getAutoRenameAgentsSetting().includes(agent.slug))
|
|
2154
|
+
return;
|
|
2155
|
+
const record = globalSessionState.sessions.find((item) => item.backendSessionId === sessionId);
|
|
2156
|
+
if (!record || (record.customName === true && !record.autoTitle))
|
|
2157
|
+
return;
|
|
2158
|
+
if (record.autoTitle && Date.now() - record.autoTitle.updatedAt < AUTO_TITLE_REEVALUATE_MS)
|
|
2159
|
+
return;
|
|
2160
|
+
const context = cleanTerminalContext(rawContext);
|
|
2161
|
+
if (context.length < 120)
|
|
2162
|
+
return;
|
|
2163
|
+
const contentHash = createHash('sha256').update(context).digest('hex');
|
|
2164
|
+
if (record.autoTitle?.contentHash === contentHash)
|
|
2165
|
+
return;
|
|
2166
|
+
autoTitlePending.add(sessionId);
|
|
2167
|
+
try {
|
|
2168
|
+
const savedModels = getAutoRenameModelsSetting();
|
|
2169
|
+
const catalog = await getTitleNamerCatalog();
|
|
2170
|
+
const availableModels = new Map(catalog.map((namer) => [
|
|
2171
|
+
namer.slug,
|
|
2172
|
+
new Set(namer.models.map((model) => model.id)),
|
|
2173
|
+
]));
|
|
2174
|
+
const validModels = Object.fromEntries(Object.entries(savedModels)
|
|
2175
|
+
.filter(([slug, model]) => availableModels.get(slug)?.has(model)));
|
|
2176
|
+
for (const namer of catalog) {
|
|
2177
|
+
if (!validModels[namer.slug] && namer.recommendedModel) {
|
|
2178
|
+
validModels[namer.slug] = namer.recommendedModel;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
const title = await generateAgentTitle(agent.slug, agent.displayName, context, {
|
|
2182
|
+
namer: getAutoRenameNamerSetting(),
|
|
2183
|
+
models: validModels,
|
|
2184
|
+
currentTitle: record.autoTitle ? record.name : undefined,
|
|
2185
|
+
});
|
|
2186
|
+
if (!title)
|
|
2187
|
+
return;
|
|
2188
|
+
// A manual rename performed while the namer was running always wins.
|
|
2189
|
+
const current = globalSessionState.sessions.find((item) => item.backendSessionId === sessionId);
|
|
2190
|
+
if (!current || (current.customName === true && !current.autoTitle))
|
|
2191
|
+
return;
|
|
2192
|
+
if (current.autoTitle && !shouldReplaceAutoTitle(current.name, title)) {
|
|
2193
|
+
upsertGlobalSessionRecord({
|
|
2194
|
+
...current,
|
|
2195
|
+
autoTitle: { agentSlug: agent.slug, contentHash, updatedAt: Date.now() },
|
|
2196
|
+
});
|
|
2197
|
+
await persistGlobalStateNow();
|
|
2198
|
+
console.log(`[auto-title] kept ${sessionId} title ${JSON.stringify(current.name)}`);
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
const next = {
|
|
2202
|
+
...current,
|
|
2203
|
+
name: title,
|
|
2204
|
+
customName: true,
|
|
2205
|
+
autoTitle: { agentSlug: agent.slug, contentHash, updatedAt: Date.now() },
|
|
2206
|
+
};
|
|
2207
|
+
if (next.mode === 'tmux' && next.tmuxSessionName) {
|
|
2208
|
+
await setTmuxOption(next.tmuxSessionName, '@termdock-friendly-name', title);
|
|
2209
|
+
}
|
|
2210
|
+
upsertGlobalSessionRecord(next);
|
|
2211
|
+
await persistGlobalStateNow();
|
|
2212
|
+
broadcastClientState();
|
|
2213
|
+
console.log(`[auto-title] renamed ${sessionId} (${agent.slug}) to ${JSON.stringify(title)}`);
|
|
2214
|
+
}
|
|
2215
|
+
catch (error) {
|
|
2216
|
+
console.warn(`[auto-title] failed for ${sessionId}: ${getErrorMessage(error)}`);
|
|
2217
|
+
}
|
|
2218
|
+
finally {
|
|
2219
|
+
autoTitlePending.delete(sessionId);
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2057
2222
|
/** Status → indicator glyph. Colors stay client-side (CSS theme vars) so
|
|
2058
2223
|
* dark/light themes render correctly; the wire carries semantics only. */
|
|
2059
2224
|
const AGENT_STATUS_INDICATOR = {
|
|
@@ -2074,7 +2239,8 @@ function isPaneAtShellPrompt(session) {
|
|
|
2074
2239
|
function buildAgentStatusPayload(sessionId, session) {
|
|
2075
2240
|
const state = session.agentSession;
|
|
2076
2241
|
const status = state ? state.status : null;
|
|
2077
|
-
const
|
|
2242
|
+
const presentation = state?.presentation ?? null;
|
|
2243
|
+
const indicator = presentation?.indicator ?? (status ? AGENT_STATUS_INDICATOR[status] : null);
|
|
2078
2244
|
let agent = session.agent;
|
|
2079
2245
|
let nativeSessionId = state?.sessionId ?? null;
|
|
2080
2246
|
// The agent just exited: keep offering its last conversation for resume
|
|
@@ -2094,6 +2260,9 @@ function buildAgentStatusPayload(sessionId, session) {
|
|
|
2094
2260
|
type: 'agent-status',
|
|
2095
2261
|
agentStatus: status,
|
|
2096
2262
|
agentIndicator: indicator ?? null,
|
|
2263
|
+
agentStatusDetail: presentation
|
|
2264
|
+
? { id: presentation.id, label: presentation.label, tone: presentation.tone ?? 'neutral' }
|
|
2265
|
+
: null,
|
|
2097
2266
|
agent: agent
|
|
2098
2267
|
? { slug: agent.slug, displayName: agent.displayName, accentColor: agent.accentColor, icon: agent.icon, isPlugin: agent.isPlugin ?? false, iconMode: agent.iconMode, iconVersion: agent.iconVersion }
|
|
2099
2268
|
: null,
|
|
@@ -2218,6 +2387,7 @@ function applyAgentSignals(sessionId, session, events, notification) {
|
|
|
2218
2387
|
// that a poll blip may have started in syncAgentIdentity.
|
|
2219
2388
|
session.agentLeftAt = null;
|
|
2220
2389
|
let identityChanged = false;
|
|
2390
|
+
let completedTurnAgent = null;
|
|
2221
2391
|
for (const event of events) {
|
|
2222
2392
|
// An event naming an agent brands the pane even when the process poll
|
|
2223
2393
|
// can't see through a wrapper: identity via protocol.
|
|
@@ -2227,7 +2397,25 @@ function applyAgentSignals(sessionId, session, events, notification) {
|
|
|
2227
2397
|
}
|
|
2228
2398
|
const state = session.agentSession ?? defaultAgentSessionState();
|
|
2229
2399
|
session.agentSession = state;
|
|
2400
|
+
const previousNativeSessionId = state.sessionId
|
|
2401
|
+
?? globalSessionState.sessions.find((item) => item.backendSessionId === sessionId)?.agentResume?.sessionId
|
|
2402
|
+
?? null;
|
|
2403
|
+
if (event.kind === 'session-start' && isNewAgentSessionId(previousNativeSessionId, event.sessionId)) {
|
|
2404
|
+
clearAutoTitleForNewAgentSession(sessionId, session);
|
|
2405
|
+
}
|
|
2406
|
+
if (event.kind === 'prompt-submit') {
|
|
2407
|
+
// Before the first automatic title, keep accumulating short turns until
|
|
2408
|
+
// there is enough substance to name the session. Once titled, only the
|
|
2409
|
+
// latest turn is relevant to the conservative re-evaluation path.
|
|
2410
|
+
const persisted = globalSessionState.sessions.find((item) => item.backendSessionId === sessionId);
|
|
2411
|
+
if (persisted?.autoTitle) {
|
|
2412
|
+
session.autoTitleContext = '';
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2230
2415
|
applyAgentEvent(state, event);
|
|
2416
|
+
if (event.kind === 'stop') {
|
|
2417
|
+
completedTurnAgent = event.agent ?? session.agent;
|
|
2418
|
+
}
|
|
2231
2419
|
if (event.kind === 'session-start' || event.kind === 'session-end' || event.kind === 'stop') {
|
|
2232
2420
|
persistAgentResumeBinding(sessionId, session);
|
|
2233
2421
|
}
|
|
@@ -2259,6 +2447,13 @@ function applyAgentSignals(sessionId, session, events, notification) {
|
|
|
2259
2447
|
// one of the git probe's triggers.
|
|
2260
2448
|
maybeRefreshGitStatusForAgent(sessionId, session);
|
|
2261
2449
|
broadcastAgentStatus(sessionId, session, identityChanged);
|
|
2450
|
+
if (completedTurnAgent) {
|
|
2451
|
+
const agent = completedTurnAgent;
|
|
2452
|
+
// Let the current PTY chunk reach the rolling context before reading it.
|
|
2453
|
+
setTimeout(() => {
|
|
2454
|
+
void maybeAutoRenameSession(sessionId, agent, session.autoTitleContext);
|
|
2455
|
+
}, 250).unref?.();
|
|
2456
|
+
}
|
|
2262
2457
|
}
|
|
2263
2458
|
// ── Git status (branch + diff size) ──
|
|
2264
2459
|
//
|
|
@@ -2407,6 +2602,9 @@ try {
|
|
|
2407
2602
|
}
|
|
2408
2603
|
for (const err of errors) {
|
|
2409
2604
|
console.warn(`[agent-plugins] validation error in "${err.slug}": ${err.errors.join('; ')}`);
|
|
2605
|
+
if (err.migration?.aiPrompt) {
|
|
2606
|
+
console.warn(`[agent-plugins] AI migration prompt for "${err.slug}": ${err.migration.aiPrompt}`);
|
|
2607
|
+
}
|
|
2410
2608
|
}
|
|
2411
2609
|
}
|
|
2412
2610
|
catch { /* plugin loading must never block startup */ }
|
|
@@ -3376,6 +3574,7 @@ function processSessionOutput(sessionId, session, data, opts) {
|
|
|
3376
3574
|
}
|
|
3377
3575
|
catch { /* sniff failure should never block data */ }
|
|
3378
3576
|
if (opts.broadcast) {
|
|
3577
|
+
appendAutoTitleContext(session, data);
|
|
3379
3578
|
broadcastEvent(sessionId, seq !== undefined ? { type: 'data', data, seq } : { type: 'data', data });
|
|
3380
3579
|
}
|
|
3381
3580
|
}
|
|
@@ -3546,6 +3745,7 @@ async function spawnTerminalSession(req, input) {
|
|
|
3546
3745
|
activeProgram: null,
|
|
3547
3746
|
agent: null,
|
|
3548
3747
|
agentSession: null,
|
|
3748
|
+
autoTitleContext: '',
|
|
3549
3749
|
lastAgentReviewed: null,
|
|
3550
3750
|
agentLeftAt: null,
|
|
3551
3751
|
gitStatus: null,
|
|
@@ -3627,6 +3827,7 @@ async function adoptPtyHostSessions() {
|
|
|
3627
3827
|
activeProgram: null,
|
|
3628
3828
|
agent: null,
|
|
3629
3829
|
agentSession: null,
|
|
3830
|
+
autoTitleContext: '',
|
|
3630
3831
|
lastAgentReviewed: null,
|
|
3631
3832
|
agentLeftAt: null,
|
|
3632
3833
|
gitStatus: null,
|
|
@@ -4036,9 +4237,11 @@ router.patch('/session-inventory/sessions/:frontendSessionId', async (req, res)
|
|
|
4036
4237
|
const next = { ...previous, lastActivity: Date.now() };
|
|
4037
4238
|
if (typeof body.name === 'string' && body.name.trim().length > 0) {
|
|
4038
4239
|
next.name = body.name.trim();
|
|
4240
|
+
next.autoTitle = null;
|
|
4039
4241
|
}
|
|
4040
4242
|
if (typeof body.customName === 'boolean') {
|
|
4041
4243
|
next.customName = body.customName ? true : undefined;
|
|
4244
|
+
next.autoTitle = null;
|
|
4042
4245
|
}
|
|
4043
4246
|
if (Object.prototype.hasOwnProperty.call(body, 'backendSessionId')) {
|
|
4044
4247
|
next.backendSessionId = typeof body.backendSessionId === 'string' && body.backendSessionId.trim().length > 0
|
|
@@ -4166,6 +4369,9 @@ async function getSettingsPayload() {
|
|
|
4166
4369
|
networkAvailable: caffeinateManager.isNetworkAvailable(),
|
|
4167
4370
|
locale: getLocaleSetting(),
|
|
4168
4371
|
contextDraftHeight: getContextDraftHeightSetting(),
|
|
4372
|
+
autoRenameAgents: getAutoRenameAgentsSetting(),
|
|
4373
|
+
autoRenameNamer: getAutoRenameNamerSetting(),
|
|
4374
|
+
autoRenameModels: getAutoRenameModelsSetting(),
|
|
4169
4375
|
localAccess: {
|
|
4170
4376
|
...localAccess,
|
|
4171
4377
|
interfaces,
|
|
@@ -4177,6 +4383,25 @@ async function getSettingsPayload() {
|
|
|
4177
4383
|
router.get('/settings', async (_req, res) => {
|
|
4178
4384
|
res.json(await getSettingsPayload());
|
|
4179
4385
|
});
|
|
4386
|
+
router.get('/auto-title/catalog', async (req, res) => {
|
|
4387
|
+
const force = req.query.refresh === '1';
|
|
4388
|
+
res.json({ namers: await getTitleNamerCatalog(force) });
|
|
4389
|
+
});
|
|
4390
|
+
router.get('/update', (_req, res) => {
|
|
4391
|
+
res.json(npmAutoUpdateManager.getState());
|
|
4392
|
+
});
|
|
4393
|
+
router.post('/update/check', async (_req, res) => {
|
|
4394
|
+
res.json(await npmAutoUpdateManager.checkNow());
|
|
4395
|
+
});
|
|
4396
|
+
router.post('/update/restart', (_req, res) => {
|
|
4397
|
+
try {
|
|
4398
|
+
const state = npmAutoUpdateManager.confirmRestart();
|
|
4399
|
+
res.status(202).json(state);
|
|
4400
|
+
}
|
|
4401
|
+
catch (error) {
|
|
4402
|
+
res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
|
|
4403
|
+
}
|
|
4404
|
+
});
|
|
4180
4405
|
router.put('/settings', async (req, res) => {
|
|
4181
4406
|
const body = req.body ?? {};
|
|
4182
4407
|
if (typeof body.preventSleep === 'boolean') {
|
|
@@ -4197,6 +4422,36 @@ router.put('/settings', async (req, res) => {
|
|
|
4197
4422
|
}
|
|
4198
4423
|
}
|
|
4199
4424
|
}
|
|
4425
|
+
if (Array.isArray(body.autoRenameAgents)) {
|
|
4426
|
+
const knownSlugs = new Set(listAgents().map((agent) => agent.slug));
|
|
4427
|
+
const requested = body.autoRenameAgents
|
|
4428
|
+
.filter((slug) => typeof slug === 'string' && knownSlugs.has(slug));
|
|
4429
|
+
setAutoRenameAgentsSetting(requested);
|
|
4430
|
+
}
|
|
4431
|
+
if (typeof body.autoRenameNamer === 'string') {
|
|
4432
|
+
if (!['auto', 'codex', 'claude'].includes(body.autoRenameNamer)) {
|
|
4433
|
+
res.status(400).json({ error: 'Unsupported automatic title agent', code: 'AUTO_RENAME_NAMER_INVALID' });
|
|
4434
|
+
return;
|
|
4435
|
+
}
|
|
4436
|
+
setAutoRenameNamerSetting(body.autoRenameNamer);
|
|
4437
|
+
}
|
|
4438
|
+
if (body.autoRenameModels && typeof body.autoRenameModels === 'object') {
|
|
4439
|
+
const requested = body.autoRenameModels;
|
|
4440
|
+
const catalog = await getTitleNamerCatalog();
|
|
4441
|
+
const allowed = new Map(catalog.map((namer) => [namer.slug, new Set(namer.models.map((model) => model.id))]));
|
|
4442
|
+
const models = {};
|
|
4443
|
+
for (const slug of ['codex', 'claude']) {
|
|
4444
|
+
const model = requested[slug];
|
|
4445
|
+
if (model === undefined || model === '')
|
|
4446
|
+
continue;
|
|
4447
|
+
if (typeof model !== 'string' || !allowed.get(slug)?.has(model)) {
|
|
4448
|
+
res.status(400).json({ error: `Unsupported ${slug} title model`, code: 'AUTO_RENAME_MODEL_INVALID' });
|
|
4449
|
+
return;
|
|
4450
|
+
}
|
|
4451
|
+
models[slug] = model;
|
|
4452
|
+
}
|
|
4453
|
+
setAutoRenameModelsSetting(models);
|
|
4454
|
+
}
|
|
4200
4455
|
if (body.localAccess && typeof body.localAccess === 'object') {
|
|
4201
4456
|
const localAccessBody = body.localAccess;
|
|
4202
4457
|
if (localAccessBody.reset === true) {
|
|
@@ -4242,6 +4497,77 @@ router.put('/context-draft', (req, res) => {
|
|
|
4242
4497
|
router.get('/agent-hooks', (_req, res) => {
|
|
4243
4498
|
res.json({ agents: listAllHookAgents() });
|
|
4244
4499
|
});
|
|
4500
|
+
router.get('/agent-launchers', async (_req, res) => {
|
|
4501
|
+
const pathEntries = buildAugmentedPath().split(path.delimiter).filter(Boolean);
|
|
4502
|
+
const executableExtensions = process.platform === 'win32'
|
|
4503
|
+
? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';')
|
|
4504
|
+
: [''];
|
|
4505
|
+
const detected = [];
|
|
4506
|
+
for (const agent of listAgents()) {
|
|
4507
|
+
let command = null;
|
|
4508
|
+
for (const alias of agent.aliases) {
|
|
4509
|
+
for (const directory of pathEntries) {
|
|
4510
|
+
for (const extension of executableExtensions) {
|
|
4511
|
+
const candidate = path.join(directory, `${alias}${extension}`);
|
|
4512
|
+
try {
|
|
4513
|
+
await fs.promises.access(candidate, process.platform === 'win32' ? fs.constants.F_OK : fs.constants.X_OK);
|
|
4514
|
+
command = alias;
|
|
4515
|
+
break;
|
|
4516
|
+
}
|
|
4517
|
+
catch { /* keep looking */ }
|
|
4518
|
+
}
|
|
4519
|
+
if (command)
|
|
4520
|
+
break;
|
|
4521
|
+
}
|
|
4522
|
+
if (command)
|
|
4523
|
+
break;
|
|
4524
|
+
}
|
|
4525
|
+
if (command)
|
|
4526
|
+
detected.push({ slug: agent.slug, displayName: agent.displayName, command });
|
|
4527
|
+
}
|
|
4528
|
+
res.json({ agents: detected });
|
|
4529
|
+
});
|
|
4530
|
+
router.get('/directory-suggestions', async (req, res) => {
|
|
4531
|
+
const rawQuery = typeof req.query.q === 'string' ? req.query.q.trim() : '';
|
|
4532
|
+
const expandedQuery = rawQuery === '~'
|
|
4533
|
+
? os.homedir()
|
|
4534
|
+
: rawQuery.startsWith(`~${path.sep}`)
|
|
4535
|
+
? path.join(os.homedir(), rawQuery.slice(2))
|
|
4536
|
+
: rawQuery;
|
|
4537
|
+
const absoluteQuery = expandedQuery || os.homedir();
|
|
4538
|
+
const hasTrailingSeparator = absoluteQuery.endsWith(path.sep);
|
|
4539
|
+
const parentCandidate = hasTrailingSeparator ? absoluteQuery : path.dirname(absoluteQuery);
|
|
4540
|
+
const namePrefix = hasTrailingSeparator ? '' : path.basename(absoluteQuery).toLowerCase();
|
|
4541
|
+
try {
|
|
4542
|
+
const parent = req.pathValidator
|
|
4543
|
+
? await req.pathValidator.validateAsync(parentCandidate)
|
|
4544
|
+
: await fs.promises.realpath(parentCandidate);
|
|
4545
|
+
const entries = await fs.promises.readdir(parent, { withFileTypes: true });
|
|
4546
|
+
const matching = entries
|
|
4547
|
+
.filter((entry) => !entry.name.startsWith('.') && entry.name.toLowerCase().startsWith(namePrefix))
|
|
4548
|
+
.slice(0, 30);
|
|
4549
|
+
const directories = [];
|
|
4550
|
+
for (const entry of matching) {
|
|
4551
|
+
const candidate = path.join(parent, entry.name);
|
|
4552
|
+
if (entry.isDirectory()) {
|
|
4553
|
+
directories.push(candidate);
|
|
4554
|
+
}
|
|
4555
|
+
else if (entry.isSymbolicLink()) {
|
|
4556
|
+
try {
|
|
4557
|
+
if ((await fs.promises.stat(candidate)).isDirectory())
|
|
4558
|
+
directories.push(candidate);
|
|
4559
|
+
}
|
|
4560
|
+
catch { /* omit broken/inaccessible symlinks */ }
|
|
4561
|
+
}
|
|
4562
|
+
if (directories.length >= 10)
|
|
4563
|
+
break;
|
|
4564
|
+
}
|
|
4565
|
+
res.json({ directories });
|
|
4566
|
+
}
|
|
4567
|
+
catch {
|
|
4568
|
+
res.json({ directories: [] });
|
|
4569
|
+
}
|
|
4570
|
+
});
|
|
4245
4571
|
router.post('/agent-hooks/:agent/install', async (req, res) => {
|
|
4246
4572
|
const agent = req.params.agent;
|
|
4247
4573
|
try {
|
|
@@ -4277,7 +4603,7 @@ router.get('/agent-plugins', (_req, res) => {
|
|
|
4277
4603
|
hasResume: p.manifest.resume !== undefined,
|
|
4278
4604
|
hasIcon: p.iconPath !== null,
|
|
4279
4605
|
})),
|
|
4280
|
-
errors: errors.map((e) => ({ slug: e.slug, errors: e.errors })),
|
|
4606
|
+
errors: errors.map((e) => ({ slug: e.slug, errors: e.errors, code: e.code, migration: e.migration })),
|
|
4281
4607
|
});
|
|
4282
4608
|
});
|
|
4283
4609
|
router.post('/agent-plugins', async (req, res) => {
|
|
@@ -4295,8 +4621,20 @@ router.post('/agent-plugins', async (req, res) => {
|
|
|
4295
4621
|
res.status(409).json({ error: `Plugin "${existing.manifest.slug}" already exists` });
|
|
4296
4622
|
return;
|
|
4297
4623
|
}
|
|
4298
|
-
const
|
|
4299
|
-
|
|
4624
|
+
const validation = validateManifest(manifest, path.join(os.homedir(), '.termdock', 'agent-plugins', 'pending'));
|
|
4625
|
+
if ('error' in validation) {
|
|
4626
|
+
res.status(400).json({
|
|
4627
|
+
error: validation.error.errors.join('\n'),
|
|
4628
|
+
code: validation.error.code ?? 'AGENT_PLUGIN_MANIFEST_INVALID',
|
|
4629
|
+
migration: validation.error.migration,
|
|
4630
|
+
});
|
|
4631
|
+
return;
|
|
4632
|
+
}
|
|
4633
|
+
const dir = savePlugin(validation.manifest);
|
|
4634
|
+
clearPluginAgents();
|
|
4635
|
+
const loaded = loadPlugins();
|
|
4636
|
+
registerPluginAgents(loaded.plugins);
|
|
4637
|
+
res.json({ slug: validation.manifest.slug, dir, state: 'created' });
|
|
4300
4638
|
}
|
|
4301
4639
|
catch (error) {
|
|
4302
4640
|
res.status(400).json({ error: error.message });
|
|
@@ -4306,6 +4644,8 @@ router.delete('/agent-plugins/:slug', async (req, res) => {
|
|
|
4306
4644
|
const { slug } = req.params;
|
|
4307
4645
|
try {
|
|
4308
4646
|
removePlugin(slug);
|
|
4647
|
+
clearPluginAgents();
|
|
4648
|
+
registerPluginAgents(loadPlugins().plugins);
|
|
4309
4649
|
res.json({ slug, state: 'removed' });
|
|
4310
4650
|
}
|
|
4311
4651
|
catch (error) {
|
|
@@ -5508,6 +5848,7 @@ export function handleControlWebSocket(ws, clientId) {
|
|
|
5508
5848
|
}
|
|
5509
5849
|
try {
|
|
5510
5850
|
ws.send(JSON.stringify({ type: 'client-state', seq: initialSeq, state: globalSessionState, inventory: inventory ?? latestSessionInventory }));
|
|
5851
|
+
ws.send(JSON.stringify({ type: 'update-state', state: npmAutoUpdateManager.getState() }));
|
|
5511
5852
|
}
|
|
5512
5853
|
catch {
|
|
5513
5854
|
controlClients.delete(clientId);
|