termdock 1.4.69 → 1.4.71
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-CG6vLwyw.js} +1 -1
- package/dist/client/assets/{abnfDiagram-VRR7QNED-BYiQXGYk.js → abnfDiagram-VRR7QNED-CrXEZLWX.js} +1 -1
- package/dist/client/assets/{arc-jlT_jmoZ.js → arc-DbKGR7Ex.js} +1 -1
- package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-D1qZ875Q.js → architectureDiagram-ZJ3FMSHR-PF5BoUbI.js} +1 -1
- package/dist/client/assets/{blockDiagram-677ZJIJ3-qkgn2Ysc.js → blockDiagram-677ZJIJ3-D1gRzVWB.js} +1 -1
- package/dist/client/assets/{c4Diagram-LMCZKHZV-JgcH49Pf.js → c4Diagram-LMCZKHZV-CwvOLync.js} +1 -1
- package/dist/client/assets/channel-JTEuKVy6.js +1 -0
- package/dist/client/assets/{chunk-2Q5K7J3B-E-3iflEy.js → chunk-2Q5K7J3B-BeRdP-xP.js} +1 -1
- package/dist/client/assets/{chunk-32BRIVSS-mM01y3kF.js → chunk-32BRIVSS-CrqR5rvl.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-GO8Muo5l.js → chunk-5VM5RSS4-CjbUr-ko.js} +1 -1
- package/dist/client/assets/{chunk-EX3LRPZG-BUSDluH0.js → chunk-EX3LRPZG-BVEk3Wma.js} +1 -1
- package/dist/client/assets/{chunk-JWPE2WC7-obSL6EzJ.js → chunk-JWPE2WC7-WitJe8kR.js} +1 -1
- package/dist/client/assets/{chunk-MOJQB5TN-zEktbtAG.js → chunk-MOJQB5TN-BCewlLYk.js} +1 -1
- package/dist/client/assets/{chunk-RYQCIY6F-kH7H8XgQ.js → chunk-RYQCIY6F-CoP0AA4G.js} +1 -1
- package/dist/client/assets/{chunk-V7JOEXUC-BqDFpHKN.js → chunk-V7JOEXUC-CSZrnmp_.js} +1 -1
- package/dist/client/assets/{chunk-VR4S4FIN-BGZ7kCIG.js → chunk-VR4S4FIN-BB-7zz-9.js} +1 -1
- package/dist/client/assets/{chunk-XXDRQBXY-D-TatWXe.js → chunk-XXDRQBXY-CcZZlCik.js} +1 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-BiFJB_5s.js +1 -0
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-BiFJB_5s.js +1 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-CfWyrViz.js → cose-bilkent-JH36ORCC-DcmzUDwh.js} +1 -1
- package/dist/client/assets/{cynefin-VYW2F7L2-BFdZ3FcE.js → cynefin-VYW2F7L2-C1XlDVoJ.js} +1 -1
- package/dist/client/assets/{cynefinDiagram-TSTJHNR4-iAfCVoFU.js → cynefinDiagram-TSTJHNR4-zY6Aet2S.js} +1 -1
- package/dist/client/assets/{dagre-VKFMJZFB-CZhh0hrr.js → dagre-VKFMJZFB-DqGTi6zR.js} +1 -1
- package/dist/client/assets/{diagram-FQU43EPY-BqUK2OmV.js → diagram-FQU43EPY-DStcsU7S.js} +1 -1
- package/dist/client/assets/{diagram-G47NLZAW-vC0pSQYC.js → diagram-G47NLZAW-Cs6IMNip.js} +1 -1
- package/dist/client/assets/{diagram-NH7WQ7WH-Dqc5oxed.js → diagram-NH7WQ7WH-yA2bpWN7.js} +1 -1
- package/dist/client/assets/{diagram-OA4YK3LP-DIIXckFE.js → diagram-OA4YK3LP-Dzf9M0PI.js} +1 -1
- package/dist/client/assets/{diagram-WEI45ONY-DR9_cFeh.js → diagram-WEI45ONY-D75lpQKA.js} +1 -1
- package/dist/client/assets/{ebnfDiagram-CCIWWBDH-21OO0OXC.js → ebnfDiagram-CCIWWBDH-CkQvIpWy.js} +1 -1
- package/dist/client/assets/{erDiagram-Q63AITRT-YKnVG3az.js → erDiagram-Q63AITRT-Ca00Vjxz.js} +1 -1
- package/dist/client/assets/{flowDiagram-23GEKE2U-Cm7TGErF.js → flowDiagram-23GEKE2U-CGrnSTEp.js} +1 -1
- package/dist/client/assets/{ganttDiagram-NO4QXBWP-D_76Effz.js → ganttDiagram-NO4QXBWP-0SHfOIP4.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-CC1ysmVD.js → gitGraphDiagram-IHSO6WYX-BeigYWvs.js} +1 -1
- package/dist/client/assets/index-D5PKdbhW.js +451 -0
- package/dist/client/assets/index-DhfIiBLW.css +1 -0
- package/dist/client/assets/{infoDiagram-FWYZ7A6U-BbgpmhBz.js → infoDiagram-FWYZ7A6U-CiY1-xjN.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-C94bY_je.js → ishikawaDiagram-FXEZZL3T-CmtDjUm_.js} +1 -1
- package/dist/client/assets/{journeyDiagram-5HDEW3XC-BQ0iFRxs.js → journeyDiagram-5HDEW3XC-B7qOr9LY.js} +1 -1
- package/dist/client/assets/{kanban-definition-HUTT4EX6-BtAexXPo.js → kanban-definition-HUTT4EX6-DO2P2Um3.js} +1 -1
- package/dist/client/assets/{linear-C2HnIdOK.js → linear-B832hJt0.js} +1 -1
- package/dist/client/assets/{mermaid.core-BbG4sJBF.js → mermaid.core-CsAzdiCU.js} +4 -4
- package/dist/client/assets/{mindmap-definition-LN4V7U3C-B-uvViws.js → mindmap-definition-LN4V7U3C-HI__vXhQ.js} +1 -1
- package/dist/client/assets/{pegDiagram-2B236MQR-DHYcR6tE.js → pegDiagram-2B236MQR--rZIwjXq.js} +1 -1
- package/dist/client/assets/{pieDiagram-ENE6RG2P-DEE9cD6M.js → pieDiagram-ENE6RG2P-D9jPf5x9.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-DMQP5jG3.js → quadrantDiagram-ABIIQ3AL-DTCP5ji2.js} +1 -1
- package/dist/client/assets/{railroadDiagram-RFXS5EU6-B-4WDTwc.js → railroadDiagram-RFXS5EU6-D2Gspvrl.js} +1 -1
- package/dist/client/assets/{requirementDiagram-TGXJPOKE-C2xw_q-R.js → requirementDiagram-TGXJPOKE-DEJC02IW.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-HTMAVEWB-wq0BPqhc.js → sankeyDiagram-HTMAVEWB-opOhKPap.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-BqpM0A9d.js → sequenceDiagram-DBY2YBRQ-_sz2CRAG.js} +1 -1
- package/dist/client/assets/{sizeCapture-X5ZJPWSS-Ba2xxyp_.js → sizeCapture-X5ZJPWSS-D0Q2QkYF.js} +1 -1
- package/dist/client/assets/{stateDiagram-2N3HPSRC-agPx5qAN.js → stateDiagram-2N3HPSRC-VgeSQTtW.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-BVh2FRO-.js +1 -0
- package/dist/client/assets/{swimlanes-5IMT3BWC-Cy6Lzj_L.js → swimlanes-5IMT3BWC-ClvBvSl0.js} +2 -2
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-C17V-kgm.js +8 -0
- package/dist/client/assets/{timeline-definition-FHXFAJF6-LBrVfbdp.js → timeline-definition-FHXFAJF6-BXNsrECV.js} +1 -1
- package/dist/client/assets/{vennDiagram-L72KCM5P-BiYIV7mX.js → vennDiagram-L72KCM5P-BgwiqZd-.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-EHGQE667-Q1QU9gG3.js → wardleyDiagram-EHGQE667-DiqN5ERx.js} +1 -1
- package/dist/client/assets/{xychartDiagram-FW5EYKEG-CzYpxk4l.js → xychartDiagram-FW5EYKEG-DQjFVqC6.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/sw.js +1 -1
- package/dist/server/agent/autoTitle.js +95 -0
- package/dist/server/agent/installers.js +106 -22
- package/dist/server/agent/plugins.js +135 -4
- package/dist/server/agent/registry.js +2 -1
- package/dist/server/agent/session.js +27 -2
- package/dist/server/agent/titleNamerCatalog.js +301 -0
- package/dist/server/agentHook.js +49 -1
- package/dist/server/cli.js +238 -30
- package/dist/server/routes/filesystem.js +11 -8
- package/dist/server/routes/terminal.js +382 -14
- package/dist/server/utils/autoUpdate.js +175 -0
- package/dist/server/utils/npmUpdate.js +2 -1
- package/dist/server/utils/settings.js +43 -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';
|
|
23
|
-
import { applyAgentEvent, defaultAgentSessionState, parseAgentEvent, } from '../agent/session.js';
|
|
22
|
+
import { agentBySlug, buildResumeCommand, detectAgentFromCommand, listAgents, } from '../agent/registry.js';
|
|
23
|
+
import { applyAgentEvent, agentEventMatchesCurrentAgent, 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 { AUTO_TITLE_MIN_CONTEXT_CHARS, cleanTerminalContext, generateAgentTitle, isNewAgentSessionId, shouldReplaceAutoTitle, } from '../agent/autoTitle.js';
|
|
32
|
+
import { getTitleNamerCatalog, invalidateTitleNamerCatalog } 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,102 @@ 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
|
+
// A new session gets one cheap attempt after its first real exchange. Short
|
|
2162
|
+
// conversations ("hi" plus a brief reply) are still valid sessions; the
|
|
2163
|
+
// one-hour re-evaluation guard below keeps this from becoming noisy after a
|
|
2164
|
+
// title exists.
|
|
2165
|
+
if (context.length < AUTO_TITLE_MIN_CONTEXT_CHARS)
|
|
2166
|
+
return;
|
|
2167
|
+
const contentHash = createHash('sha256').update(context).digest('hex');
|
|
2168
|
+
if (record.autoTitle?.contentHash === contentHash)
|
|
2169
|
+
return;
|
|
2170
|
+
autoTitlePending.add(sessionId);
|
|
2171
|
+
try {
|
|
2172
|
+
const savedModels = getAutoRenameModelsSetting();
|
|
2173
|
+
const catalog = await getTitleNamerCatalog();
|
|
2174
|
+
const availableModels = new Map(catalog.map((namer) => [
|
|
2175
|
+
namer.slug,
|
|
2176
|
+
new Set(namer.models.map((model) => model.id)),
|
|
2177
|
+
]));
|
|
2178
|
+
const validModels = Object.fromEntries(Object.entries(savedModels)
|
|
2179
|
+
.filter(([slug, model]) => availableModels.get(slug)?.has(model)));
|
|
2180
|
+
for (const namer of catalog) {
|
|
2181
|
+
if (!validModels[namer.slug] && namer.recommendedModel) {
|
|
2182
|
+
validModels[namer.slug] = namer.recommendedModel;
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
const title = await generateAgentTitle(agent.slug, agent.displayName, context, {
|
|
2186
|
+
namer: getAutoRenameNamerSetting(),
|
|
2187
|
+
models: validModels,
|
|
2188
|
+
currentTitle: record.autoTitle ? record.name : undefined,
|
|
2189
|
+
});
|
|
2190
|
+
if (!title)
|
|
2191
|
+
return;
|
|
2192
|
+
// A manual rename performed while the namer was running always wins.
|
|
2193
|
+
const current = globalSessionState.sessions.find((item) => item.backendSessionId === sessionId);
|
|
2194
|
+
if (!current || (current.customName === true && !current.autoTitle))
|
|
2195
|
+
return;
|
|
2196
|
+
if (current.autoTitle && !shouldReplaceAutoTitle(current.name, title)) {
|
|
2197
|
+
upsertGlobalSessionRecord({
|
|
2198
|
+
...current,
|
|
2199
|
+
autoTitle: { agentSlug: agent.slug, contentHash, updatedAt: Date.now() },
|
|
2200
|
+
});
|
|
2201
|
+
await persistGlobalStateNow();
|
|
2202
|
+
console.log(`[auto-title] kept ${sessionId} title ${JSON.stringify(current.name)}`);
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
const next = {
|
|
2206
|
+
...current,
|
|
2207
|
+
name: title,
|
|
2208
|
+
customName: true,
|
|
2209
|
+
autoTitle: { agentSlug: agent.slug, contentHash, updatedAt: Date.now() },
|
|
2210
|
+
};
|
|
2211
|
+
if (next.mode === 'tmux' && next.tmuxSessionName) {
|
|
2212
|
+
await setTmuxOption(next.tmuxSessionName, '@termdock-friendly-name', title);
|
|
2213
|
+
}
|
|
2214
|
+
upsertGlobalSessionRecord(next);
|
|
2215
|
+
await persistGlobalStateNow();
|
|
2216
|
+
broadcastClientState();
|
|
2217
|
+
console.log(`[auto-title] renamed ${sessionId} (${agent.slug}) to ${JSON.stringify(title)}`);
|
|
2218
|
+
}
|
|
2219
|
+
catch (error) {
|
|
2220
|
+
console.warn(`[auto-title] failed for ${sessionId}: ${getErrorMessage(error)}`);
|
|
2221
|
+
}
|
|
2222
|
+
finally {
|
|
2223
|
+
autoTitlePending.delete(sessionId);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2057
2226
|
/** Status → indicator glyph. Colors stay client-side (CSS theme vars) so
|
|
2058
2227
|
* dark/light themes render correctly; the wire carries semantics only. */
|
|
2059
2228
|
const AGENT_STATUS_INDICATOR = {
|
|
@@ -2074,7 +2243,8 @@ function isPaneAtShellPrompt(session) {
|
|
|
2074
2243
|
function buildAgentStatusPayload(sessionId, session) {
|
|
2075
2244
|
const state = session.agentSession;
|
|
2076
2245
|
const status = state ? state.status : null;
|
|
2077
|
-
const
|
|
2246
|
+
const presentation = state?.presentation ?? null;
|
|
2247
|
+
const indicator = presentation?.indicator ?? (status ? AGENT_STATUS_INDICATOR[status] : null);
|
|
2078
2248
|
let agent = session.agent;
|
|
2079
2249
|
let nativeSessionId = state?.sessionId ?? null;
|
|
2080
2250
|
// The agent just exited: keep offering its last conversation for resume
|
|
@@ -2094,6 +2264,9 @@ function buildAgentStatusPayload(sessionId, session) {
|
|
|
2094
2264
|
type: 'agent-status',
|
|
2095
2265
|
agentStatus: status,
|
|
2096
2266
|
agentIndicator: indicator ?? null,
|
|
2267
|
+
agentStatusDetail: presentation
|
|
2268
|
+
? { id: presentation.id, label: presentation.label, tone: presentation.tone ?? 'neutral' }
|
|
2269
|
+
: null,
|
|
2097
2270
|
agent: agent
|
|
2098
2271
|
? { slug: agent.slug, displayName: agent.displayName, accentColor: agent.accentColor, icon: agent.icon, isPlugin: agent.isPlugin ?? false, iconMode: agent.iconMode, iconVersion: agent.iconVersion }
|
|
2099
2272
|
: null,
|
|
@@ -2218,7 +2391,10 @@ function applyAgentSignals(sessionId, session, events, notification) {
|
|
|
2218
2391
|
// that a poll blip may have started in syncAgentIdentity.
|
|
2219
2392
|
session.agentLeftAt = null;
|
|
2220
2393
|
let identityChanged = false;
|
|
2394
|
+
let completedTurnAgent = null;
|
|
2221
2395
|
for (const event of events) {
|
|
2396
|
+
if (!agentEventMatchesCurrentAgent(session.agent, event))
|
|
2397
|
+
continue;
|
|
2222
2398
|
// An event naming an agent brands the pane even when the process poll
|
|
2223
2399
|
// can't see through a wrapper: identity via protocol.
|
|
2224
2400
|
if (!session.agent && event.agent) {
|
|
@@ -2227,7 +2403,25 @@ function applyAgentSignals(sessionId, session, events, notification) {
|
|
|
2227
2403
|
}
|
|
2228
2404
|
const state = session.agentSession ?? defaultAgentSessionState();
|
|
2229
2405
|
session.agentSession = state;
|
|
2406
|
+
const previousNativeSessionId = state.sessionId
|
|
2407
|
+
?? globalSessionState.sessions.find((item) => item.backendSessionId === sessionId)?.agentResume?.sessionId
|
|
2408
|
+
?? null;
|
|
2409
|
+
if (event.kind === 'session-start' && isNewAgentSessionId(previousNativeSessionId, event.sessionId)) {
|
|
2410
|
+
clearAutoTitleForNewAgentSession(sessionId, session);
|
|
2411
|
+
}
|
|
2412
|
+
if (event.kind === 'prompt-submit') {
|
|
2413
|
+
// Before the first automatic title, keep accumulating short turns until
|
|
2414
|
+
// there is enough substance to name the session. Once titled, only the
|
|
2415
|
+
// latest turn is relevant to the conservative re-evaluation path.
|
|
2416
|
+
const persisted = globalSessionState.sessions.find((item) => item.backendSessionId === sessionId);
|
|
2417
|
+
if (persisted?.autoTitle) {
|
|
2418
|
+
session.autoTitleContext = '';
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2230
2421
|
applyAgentEvent(state, event);
|
|
2422
|
+
if (event.kind === 'stop') {
|
|
2423
|
+
completedTurnAgent = event.agent ?? session.agent;
|
|
2424
|
+
}
|
|
2231
2425
|
if (event.kind === 'session-start' || event.kind === 'session-end' || event.kind === 'stop') {
|
|
2232
2426
|
persistAgentResumeBinding(sessionId, session);
|
|
2233
2427
|
}
|
|
@@ -2259,6 +2453,13 @@ function applyAgentSignals(sessionId, session, events, notification) {
|
|
|
2259
2453
|
// one of the git probe's triggers.
|
|
2260
2454
|
maybeRefreshGitStatusForAgent(sessionId, session);
|
|
2261
2455
|
broadcastAgentStatus(sessionId, session, identityChanged);
|
|
2456
|
+
if (completedTurnAgent) {
|
|
2457
|
+
const agent = completedTurnAgent;
|
|
2458
|
+
// Let the current PTY chunk reach the rolling context before reading it.
|
|
2459
|
+
setTimeout(() => {
|
|
2460
|
+
void maybeAutoRenameSession(sessionId, agent, session.autoTitleContext);
|
|
2461
|
+
}, 250).unref?.();
|
|
2462
|
+
}
|
|
2262
2463
|
}
|
|
2263
2464
|
// ── Git status (branch + diff size) ──
|
|
2264
2465
|
//
|
|
@@ -2407,6 +2608,9 @@ try {
|
|
|
2407
2608
|
}
|
|
2408
2609
|
for (const err of errors) {
|
|
2409
2610
|
console.warn(`[agent-plugins] validation error in "${err.slug}": ${err.errors.join('; ')}`);
|
|
2611
|
+
if (err.migration?.aiPrompt) {
|
|
2612
|
+
console.warn(`[agent-plugins] AI migration prompt for "${err.slug}": ${err.migration.aiPrompt}`);
|
|
2613
|
+
}
|
|
2410
2614
|
}
|
|
2411
2615
|
}
|
|
2412
2616
|
catch { /* plugin loading must never block startup */ }
|
|
@@ -3376,6 +3580,7 @@ function processSessionOutput(sessionId, session, data, opts) {
|
|
|
3376
3580
|
}
|
|
3377
3581
|
catch { /* sniff failure should never block data */ }
|
|
3378
3582
|
if (opts.broadcast) {
|
|
3583
|
+
appendAutoTitleContext(session, data);
|
|
3379
3584
|
broadcastEvent(sessionId, seq !== undefined ? { type: 'data', data, seq } : { type: 'data', data });
|
|
3380
3585
|
}
|
|
3381
3586
|
}
|
|
@@ -3546,6 +3751,7 @@ async function spawnTerminalSession(req, input) {
|
|
|
3546
3751
|
activeProgram: null,
|
|
3547
3752
|
agent: null,
|
|
3548
3753
|
agentSession: null,
|
|
3754
|
+
autoTitleContext: '',
|
|
3549
3755
|
lastAgentReviewed: null,
|
|
3550
3756
|
agentLeftAt: null,
|
|
3551
3757
|
gitStatus: null,
|
|
@@ -3627,6 +3833,7 @@ async function adoptPtyHostSessions() {
|
|
|
3627
3833
|
activeProgram: null,
|
|
3628
3834
|
agent: null,
|
|
3629
3835
|
agentSession: null,
|
|
3836
|
+
autoTitleContext: '',
|
|
3630
3837
|
lastAgentReviewed: null,
|
|
3631
3838
|
agentLeftAt: null,
|
|
3632
3839
|
gitStatus: null,
|
|
@@ -4036,9 +4243,11 @@ router.patch('/session-inventory/sessions/:frontendSessionId', async (req, res)
|
|
|
4036
4243
|
const next = { ...previous, lastActivity: Date.now() };
|
|
4037
4244
|
if (typeof body.name === 'string' && body.name.trim().length > 0) {
|
|
4038
4245
|
next.name = body.name.trim();
|
|
4246
|
+
next.autoTitle = null;
|
|
4039
4247
|
}
|
|
4040
4248
|
if (typeof body.customName === 'boolean') {
|
|
4041
4249
|
next.customName = body.customName ? true : undefined;
|
|
4250
|
+
next.autoTitle = null;
|
|
4042
4251
|
}
|
|
4043
4252
|
if (Object.prototype.hasOwnProperty.call(body, 'backendSessionId')) {
|
|
4044
4253
|
next.backendSessionId = typeof body.backendSessionId === 'string' && body.backendSessionId.trim().length > 0
|
|
@@ -4166,6 +4375,9 @@ async function getSettingsPayload() {
|
|
|
4166
4375
|
networkAvailable: caffeinateManager.isNetworkAvailable(),
|
|
4167
4376
|
locale: getLocaleSetting(),
|
|
4168
4377
|
contextDraftHeight: getContextDraftHeightSetting(),
|
|
4378
|
+
autoRenameAgents: getAutoRenameAgentsSetting(),
|
|
4379
|
+
autoRenameNamer: getAutoRenameNamerSetting(),
|
|
4380
|
+
autoRenameModels: getAutoRenameModelsSetting(),
|
|
4169
4381
|
localAccess: {
|
|
4170
4382
|
...localAccess,
|
|
4171
4383
|
interfaces,
|
|
@@ -4177,6 +4389,25 @@ async function getSettingsPayload() {
|
|
|
4177
4389
|
router.get('/settings', async (_req, res) => {
|
|
4178
4390
|
res.json(await getSettingsPayload());
|
|
4179
4391
|
});
|
|
4392
|
+
router.get('/auto-title/catalog', async (req, res) => {
|
|
4393
|
+
const force = req.query.refresh === '1';
|
|
4394
|
+
res.json({ namers: await getTitleNamerCatalog(force) });
|
|
4395
|
+
});
|
|
4396
|
+
router.get('/update', (_req, res) => {
|
|
4397
|
+
res.json(npmAutoUpdateManager.getState());
|
|
4398
|
+
});
|
|
4399
|
+
router.post('/update/check', async (_req, res) => {
|
|
4400
|
+
res.json(await npmAutoUpdateManager.checkNow());
|
|
4401
|
+
});
|
|
4402
|
+
router.post('/update/restart', (_req, res) => {
|
|
4403
|
+
try {
|
|
4404
|
+
const state = npmAutoUpdateManager.confirmRestart();
|
|
4405
|
+
res.status(202).json(state);
|
|
4406
|
+
}
|
|
4407
|
+
catch (error) {
|
|
4408
|
+
res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
|
|
4409
|
+
}
|
|
4410
|
+
});
|
|
4180
4411
|
router.put('/settings', async (req, res) => {
|
|
4181
4412
|
const body = req.body ?? {};
|
|
4182
4413
|
if (typeof body.preventSleep === 'boolean') {
|
|
@@ -4197,6 +4428,36 @@ router.put('/settings', async (req, res) => {
|
|
|
4197
4428
|
}
|
|
4198
4429
|
}
|
|
4199
4430
|
}
|
|
4431
|
+
if (Array.isArray(body.autoRenameAgents)) {
|
|
4432
|
+
const knownSlugs = new Set(listAgents().map((agent) => agent.slug));
|
|
4433
|
+
const requested = body.autoRenameAgents
|
|
4434
|
+
.filter((slug) => typeof slug === 'string' && knownSlugs.has(slug));
|
|
4435
|
+
setAutoRenameAgentsSetting(requested);
|
|
4436
|
+
}
|
|
4437
|
+
if (typeof body.autoRenameNamer === 'string') {
|
|
4438
|
+
const available = new Set((await getTitleNamerCatalog()).filter((entry) => entry.available).map((entry) => entry.slug));
|
|
4439
|
+
if (body.autoRenameNamer !== 'auto' && !available.has(body.autoRenameNamer)) {
|
|
4440
|
+
res.status(400).json({ error: 'Unsupported automatic title agent', code: 'AUTO_RENAME_NAMER_INVALID' });
|
|
4441
|
+
return;
|
|
4442
|
+
}
|
|
4443
|
+
setAutoRenameNamerSetting(body.autoRenameNamer);
|
|
4444
|
+
}
|
|
4445
|
+
if (body.autoRenameModels && typeof body.autoRenameModels === 'object') {
|
|
4446
|
+
const requested = body.autoRenameModels;
|
|
4447
|
+
const catalog = await getTitleNamerCatalog();
|
|
4448
|
+
const allowed = new Map(catalog.map((namer) => [namer.slug, new Set(namer.models.map((model) => model.id))]));
|
|
4449
|
+
const models = {};
|
|
4450
|
+
for (const [slug, model] of Object.entries(requested)) {
|
|
4451
|
+
if (model === undefined || model === '')
|
|
4452
|
+
continue;
|
|
4453
|
+
if (typeof model !== 'string' || !allowed.get(slug)?.has(model)) {
|
|
4454
|
+
res.status(400).json({ error: `Unsupported ${slug} title model`, code: 'AUTO_RENAME_MODEL_INVALID' });
|
|
4455
|
+
return;
|
|
4456
|
+
}
|
|
4457
|
+
models[slug] = model;
|
|
4458
|
+
}
|
|
4459
|
+
setAutoRenameModelsSetting(models);
|
|
4460
|
+
}
|
|
4200
4461
|
if (body.localAccess && typeof body.localAccess === 'object') {
|
|
4201
4462
|
const localAccessBody = body.localAccess;
|
|
4202
4463
|
if (localAccessBody.reset === true) {
|
|
@@ -4239,8 +4500,79 @@ router.put('/context-draft', (req, res) => {
|
|
|
4239
4500
|
res.json(doc);
|
|
4240
4501
|
});
|
|
4241
4502
|
// ── Agent hooks API (rich status channel installers, built-in + plugin) ──
|
|
4242
|
-
router.get('/agent-hooks', (_req, res) => {
|
|
4243
|
-
res.json({ agents: listAllHookAgents() });
|
|
4503
|
+
router.get('/agent-hooks', async (_req, res) => {
|
|
4504
|
+
res.json({ agents: await listAllHookAgents() });
|
|
4505
|
+
});
|
|
4506
|
+
router.get('/agent-launchers', async (_req, res) => {
|
|
4507
|
+
const pathEntries = buildAugmentedPath().split(path.delimiter).filter(Boolean);
|
|
4508
|
+
const executableExtensions = process.platform === 'win32'
|
|
4509
|
+
? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';')
|
|
4510
|
+
: [''];
|
|
4511
|
+
const detected = [];
|
|
4512
|
+
for (const agent of listAgents()) {
|
|
4513
|
+
let command = null;
|
|
4514
|
+
for (const alias of agent.aliases) {
|
|
4515
|
+
for (const directory of pathEntries) {
|
|
4516
|
+
for (const extension of executableExtensions) {
|
|
4517
|
+
const candidate = path.join(directory, `${alias}${extension}`);
|
|
4518
|
+
try {
|
|
4519
|
+
await fs.promises.access(candidate, process.platform === 'win32' ? fs.constants.F_OK : fs.constants.X_OK);
|
|
4520
|
+
command = alias;
|
|
4521
|
+
break;
|
|
4522
|
+
}
|
|
4523
|
+
catch { /* keep looking */ }
|
|
4524
|
+
}
|
|
4525
|
+
if (command)
|
|
4526
|
+
break;
|
|
4527
|
+
}
|
|
4528
|
+
if (command)
|
|
4529
|
+
break;
|
|
4530
|
+
}
|
|
4531
|
+
if (command)
|
|
4532
|
+
detected.push({ slug: agent.slug, displayName: agent.displayName, command });
|
|
4533
|
+
}
|
|
4534
|
+
res.json({ agents: detected });
|
|
4535
|
+
});
|
|
4536
|
+
router.get('/directory-suggestions', async (req, res) => {
|
|
4537
|
+
const rawQuery = typeof req.query.q === 'string' ? req.query.q.trim() : '';
|
|
4538
|
+
const expandedQuery = rawQuery === '~'
|
|
4539
|
+
? os.homedir()
|
|
4540
|
+
: rawQuery.startsWith(`~${path.sep}`)
|
|
4541
|
+
? path.join(os.homedir(), rawQuery.slice(2))
|
|
4542
|
+
: rawQuery;
|
|
4543
|
+
const absoluteQuery = expandedQuery || os.homedir();
|
|
4544
|
+
const hasTrailingSeparator = absoluteQuery.endsWith(path.sep);
|
|
4545
|
+
const parentCandidate = hasTrailingSeparator ? absoluteQuery : path.dirname(absoluteQuery);
|
|
4546
|
+
const namePrefix = hasTrailingSeparator ? '' : path.basename(absoluteQuery).toLowerCase();
|
|
4547
|
+
try {
|
|
4548
|
+
const parent = req.pathValidator
|
|
4549
|
+
? await req.pathValidator.validateAsync(parentCandidate)
|
|
4550
|
+
: await fs.promises.realpath(parentCandidate);
|
|
4551
|
+
const entries = await fs.promises.readdir(parent, { withFileTypes: true });
|
|
4552
|
+
const matching = entries
|
|
4553
|
+
.filter((entry) => !entry.name.startsWith('.') && entry.name.toLowerCase().startsWith(namePrefix))
|
|
4554
|
+
.slice(0, 30);
|
|
4555
|
+
const directories = [];
|
|
4556
|
+
for (const entry of matching) {
|
|
4557
|
+
const candidate = path.join(parent, entry.name);
|
|
4558
|
+
if (entry.isDirectory()) {
|
|
4559
|
+
directories.push(candidate);
|
|
4560
|
+
}
|
|
4561
|
+
else if (entry.isSymbolicLink()) {
|
|
4562
|
+
try {
|
|
4563
|
+
if ((await fs.promises.stat(candidate)).isDirectory())
|
|
4564
|
+
directories.push(candidate);
|
|
4565
|
+
}
|
|
4566
|
+
catch { /* omit broken/inaccessible symlinks */ }
|
|
4567
|
+
}
|
|
4568
|
+
if (directories.length >= 10)
|
|
4569
|
+
break;
|
|
4570
|
+
}
|
|
4571
|
+
res.json({ directories });
|
|
4572
|
+
}
|
|
4573
|
+
catch {
|
|
4574
|
+
res.json({ directories: [] });
|
|
4575
|
+
}
|
|
4244
4576
|
});
|
|
4245
4577
|
router.post('/agent-hooks/:agent/install', async (req, res) => {
|
|
4246
4578
|
const agent = req.params.agent;
|
|
@@ -4275,9 +4607,10 @@ router.get('/agent-plugins', (_req, res) => {
|
|
|
4275
4607
|
iconMode: p.manifest.iconMode ?? 'mask',
|
|
4276
4608
|
hasHooks: p.manifest.hooks !== undefined,
|
|
4277
4609
|
hasResume: p.manifest.resume !== undefined,
|
|
4610
|
+
hasTitleNamer: p.manifest.titleNamer !== undefined,
|
|
4278
4611
|
hasIcon: p.iconPath !== null,
|
|
4279
4612
|
})),
|
|
4280
|
-
errors: errors.map((e) => ({ slug: e.slug, errors: e.errors })),
|
|
4613
|
+
errors: errors.map((e) => ({ slug: e.slug, errors: e.errors, code: e.code, migration: e.migration })),
|
|
4281
4614
|
});
|
|
4282
4615
|
});
|
|
4283
4616
|
router.post('/agent-plugins', async (req, res) => {
|
|
@@ -4295,8 +4628,21 @@ router.post('/agent-plugins', async (req, res) => {
|
|
|
4295
4628
|
res.status(409).json({ error: `Plugin "${existing.manifest.slug}" already exists` });
|
|
4296
4629
|
return;
|
|
4297
4630
|
}
|
|
4298
|
-
const
|
|
4299
|
-
|
|
4631
|
+
const validation = validateManifest(manifest, path.join(os.homedir(), '.termdock', 'agent-plugins', 'pending'));
|
|
4632
|
+
if ('error' in validation) {
|
|
4633
|
+
res.status(400).json({
|
|
4634
|
+
error: validation.error.errors.join('\n'),
|
|
4635
|
+
code: validation.error.code ?? 'AGENT_PLUGIN_MANIFEST_INVALID',
|
|
4636
|
+
migration: validation.error.migration,
|
|
4637
|
+
});
|
|
4638
|
+
return;
|
|
4639
|
+
}
|
|
4640
|
+
const dir = savePlugin(validation.manifest);
|
|
4641
|
+
invalidateTitleNamerCatalog();
|
|
4642
|
+
clearPluginAgents();
|
|
4643
|
+
const loaded = loadPlugins();
|
|
4644
|
+
registerPluginAgents(loaded.plugins);
|
|
4645
|
+
res.json({ slug: validation.manifest.slug, dir, state: 'created' });
|
|
4300
4646
|
}
|
|
4301
4647
|
catch (error) {
|
|
4302
4648
|
res.status(400).json({ error: error.message });
|
|
@@ -4305,11 +4651,32 @@ router.post('/agent-plugins', async (req, res) => {
|
|
|
4305
4651
|
router.delete('/agent-plugins/:slug', async (req, res) => {
|
|
4306
4652
|
const { slug } = req.params;
|
|
4307
4653
|
try {
|
|
4654
|
+
const plugin = loadPlugins().plugins.find((entry) => entry.manifest.slug === slug);
|
|
4655
|
+
if (!plugin) {
|
|
4656
|
+
res.status(404).json({ error: `Plugin "${slug}" not found` });
|
|
4657
|
+
return;
|
|
4658
|
+
}
|
|
4659
|
+
// Uninstall while the manifest is still present: after deletion we no
|
|
4660
|
+
// longer know the target file or event shape and would orphan commands in
|
|
4661
|
+
// the Agent's config.
|
|
4662
|
+
if (plugin.manifest.hooks)
|
|
4663
|
+
await uninstallHooksForSlug(slug);
|
|
4308
4664
|
removePlugin(slug);
|
|
4665
|
+
invalidateTitleNamerCatalog();
|
|
4666
|
+
setAutoRenameAgentsSetting(getAutoRenameAgentsSetting().filter((entry) => entry !== slug));
|
|
4667
|
+
if (getAutoRenameNamerSetting() === slug)
|
|
4668
|
+
setAutoRenameNamerSetting('auto');
|
|
4669
|
+
const models = getAutoRenameModelsSetting();
|
|
4670
|
+
if (models[slug]) {
|
|
4671
|
+
delete models[slug];
|
|
4672
|
+
setAutoRenameModelsSetting(models);
|
|
4673
|
+
}
|
|
4674
|
+
clearPluginAgents();
|
|
4675
|
+
registerPluginAgents(loadPlugins().plugins);
|
|
4309
4676
|
res.json({ slug, state: 'removed' });
|
|
4310
4677
|
}
|
|
4311
4678
|
catch (error) {
|
|
4312
|
-
res.status(
|
|
4679
|
+
res.status(400).json({ error: error.message });
|
|
4313
4680
|
}
|
|
4314
4681
|
});
|
|
4315
4682
|
router.get('/agent-plugin-icon/:slug', (req, res) => {
|
|
@@ -5508,6 +5875,7 @@ export function handleControlWebSocket(ws, clientId) {
|
|
|
5508
5875
|
}
|
|
5509
5876
|
try {
|
|
5510
5877
|
ws.send(JSON.stringify({ type: 'client-state', seq: initialSeq, state: globalSessionState, inventory: inventory ?? latestSessionInventory }));
|
|
5878
|
+
ws.send(JSON.stringify({ type: 'update-state', state: npmAutoUpdateManager.getState() }));
|
|
5511
5879
|
}
|
|
5512
5880
|
catch {
|
|
5513
5881
|
controlClients.delete(clientId);
|