termdock 1.4.70 → 1.4.72

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.
Files changed (81) hide show
  1. package/README.md +36 -0
  2. package/dist/client/assets/{ModelPreview-Cs14IQuQ.js → ModelPreview-i9dS1A3I.js} +1 -1
  3. package/dist/client/assets/{abnfDiagram-VRR7QNED-DuDfdv-6.js → abnfDiagram-VRR7QNED-Cs-ty-HA.js} +1 -1
  4. package/dist/client/assets/{arc-CSYzGV_2.js → arc-CIX5k_JD.js} +1 -1
  5. package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-YiZ0HXNi.js → architectureDiagram-ZJ3FMSHR-DnPnQoJy.js} +1 -1
  6. package/dist/client/assets/{blockDiagram-677ZJIJ3-IaQMzmYv.js → blockDiagram-677ZJIJ3-Dhew-L5v.js} +1 -1
  7. package/dist/client/assets/{c4Diagram-LMCZKHZV-BwyuIPRL.js → c4Diagram-LMCZKHZV-BSsEMeWn.js} +1 -1
  8. package/dist/client/assets/channel-CASnt1pP.js +1 -0
  9. package/dist/client/assets/{chunk-2Q5K7J3B-BtHQYLBT.js → chunk-2Q5K7J3B-BWm--1Mo.js} +1 -1
  10. package/dist/client/assets/{chunk-32BRIVSS-CXrK07sW.js → chunk-32BRIVSS-D_WFmuvb.js} +1 -1
  11. package/dist/client/assets/{chunk-5VM5RSS4-BfBshn11.js → chunk-5VM5RSS4-CQdzqlQg.js} +1 -1
  12. package/dist/client/assets/{chunk-EX3LRPZG-BJbilNaf.js → chunk-EX3LRPZG-Cm3zLQpt.js} +1 -1
  13. package/dist/client/assets/{chunk-JWPE2WC7-wgcmSRlL.js → chunk-JWPE2WC7-Cmcv5sEN.js} +1 -1
  14. package/dist/client/assets/{chunk-MOJQB5TN-8VZVUeDI.js → chunk-MOJQB5TN-Bam44EwJ.js} +1 -1
  15. package/dist/client/assets/{chunk-RYQCIY6F-B900YBFa.js → chunk-RYQCIY6F-DIVOuE8y.js} +1 -1
  16. package/dist/client/assets/{chunk-V7JOEXUC-BK4_RLz-.js → chunk-V7JOEXUC-BfOTf7yg.js} +1 -1
  17. package/dist/client/assets/{chunk-VR4S4FIN-Kj5_ycjB.js → chunk-VR4S4FIN-DbiXn0dd.js} +1 -1
  18. package/dist/client/assets/{chunk-XXDRQBXY-BolvPLRz.js → chunk-XXDRQBXY-C4pOe4OC.js} +1 -1
  19. package/dist/client/assets/classDiagram-OUVF2IWQ-Da_SQWEq.js +1 -0
  20. package/dist/client/assets/classDiagram-v2-EOCWNBFH-Da_SQWEq.js +1 -0
  21. package/dist/client/assets/{cose-bilkent-JH36ORCC-9_9_SLqv.js → cose-bilkent-JH36ORCC-CCoSPmoc.js} +1 -1
  22. package/dist/client/assets/{cynefin-VYW2F7L2-yEpoi3vG.js → cynefin-VYW2F7L2-B2lYz7sl.js} +1 -1
  23. package/dist/client/assets/{cynefinDiagram-TSTJHNR4-Dy-gvup_.js → cynefinDiagram-TSTJHNR4-Cd7crN9s.js} +1 -1
  24. package/dist/client/assets/{dagre-VKFMJZFB-CNjJ5pRK.js → dagre-VKFMJZFB-q9Fj9HuQ.js} +1 -1
  25. package/dist/client/assets/{diagram-FQU43EPY-uZ73Rj3T.js → diagram-FQU43EPY-iqHy5YHW.js} +1 -1
  26. package/dist/client/assets/{diagram-G47NLZAW-Cxt7TYZT.js → diagram-G47NLZAW-BE-jlh1B.js} +1 -1
  27. package/dist/client/assets/{diagram-NH7WQ7WH-B4TKinuZ.js → diagram-NH7WQ7WH-DmP7_WOC.js} +1 -1
  28. package/dist/client/assets/{diagram-OA4YK3LP-CGSuenHU.js → diagram-OA4YK3LP-CHx_k8Ar.js} +1 -1
  29. package/dist/client/assets/{diagram-WEI45ONY-DqqzDaUZ.js → diagram-WEI45ONY-D0wImpAF.js} +1 -1
  30. package/dist/client/assets/{ebnfDiagram-CCIWWBDH-B2SgXL5w.js → ebnfDiagram-CCIWWBDH-DyCB0rF8.js} +1 -1
  31. package/dist/client/assets/{erDiagram-Q63AITRT-D-ibkr8y.js → erDiagram-Q63AITRT-SRcd1B2Z.js} +1 -1
  32. package/dist/client/assets/{flowDiagram-23GEKE2U-BPeyMqr-.js → flowDiagram-23GEKE2U-D0i9mFwt.js} +1 -1
  33. package/dist/client/assets/{ganttDiagram-NO4QXBWP-DcEqEzzu.js → ganttDiagram-NO4QXBWP-BfwGIYDn.js} +1 -1
  34. package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-DTZ6UCQY.js → gitGraphDiagram-IHSO6WYX-BXkJQP7O.js} +1 -1
  35. package/dist/client/assets/index-BA-DqJM2.js +451 -0
  36. package/dist/client/assets/{infoDiagram-FWYZ7A6U-BHrzVXeX.js → infoDiagram-FWYZ7A6U-CM-Ebv9R.js} +1 -1
  37. package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-CwuoQJku.js → ishikawaDiagram-FXEZZL3T-U-qnP7xi.js} +1 -1
  38. package/dist/client/assets/{journeyDiagram-5HDEW3XC-ZEcy0rDZ.js → journeyDiagram-5HDEW3XC-D4RxvsuL.js} +1 -1
  39. package/dist/client/assets/{kanban-definition-HUTT4EX6-BDBawNbr.js → kanban-definition-HUTT4EX6-Dm_asCYK.js} +1 -1
  40. package/dist/client/assets/{linear-smYQWzDV.js → linear-Zhe8d635.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-eJs7Ouvw.js → mermaid.core-D_NWIuMY.js} +4 -4
  42. package/dist/client/assets/{mindmap-definition-LN4V7U3C-BK1IZ9zg.js → mindmap-definition-LN4V7U3C-DXpNidd5.js} +1 -1
  43. package/dist/client/assets/{pegDiagram-2B236MQR-B3-mVNgX.js → pegDiagram-2B236MQR-DdOwOEgu.js} +1 -1
  44. package/dist/client/assets/{pieDiagram-ENE6RG2P-Bkeeyx5l.js → pieDiagram-ENE6RG2P-D_A9HoY_.js} +1 -1
  45. package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-DouIklEO.js → quadrantDiagram-ABIIQ3AL-D-Y3cLEF.js} +1 -1
  46. package/dist/client/assets/{railroadDiagram-RFXS5EU6-BB_jXQd2.js → railroadDiagram-RFXS5EU6-DD6eFV82.js} +1 -1
  47. package/dist/client/assets/{requirementDiagram-TGXJPOKE-u56IHfiR.js → requirementDiagram-TGXJPOKE-B59HJgL2.js} +1 -1
  48. package/dist/client/assets/{sankeyDiagram-HTMAVEWB-eDi2cs5E.js → sankeyDiagram-HTMAVEWB-C2BvHMH8.js} +1 -1
  49. package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-DnAA2xYa.js → sequenceDiagram-DBY2YBRQ-3004tCRm.js} +1 -1
  50. package/dist/client/assets/{sizeCapture-X5ZJPWSS-BogPui79.js → sizeCapture-X5ZJPWSS-DCBhg8QW.js} +1 -1
  51. package/dist/client/assets/{stateDiagram-2N3HPSRC--EeXDy2M.js → stateDiagram-2N3HPSRC-BVqiKVe0.js} +1 -1
  52. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-Z0GHWyoF.js +1 -0
  53. package/dist/client/assets/{swimlanes-5IMT3BWC-DZAw7-kY.js → swimlanes-5IMT3BWC-DS6-sdEW.js} +2 -2
  54. package/dist/client/assets/swimlanesDiagram-G3AALYLV-el4HDX0G.js +8 -0
  55. package/dist/client/assets/{timeline-definition-FHXFAJF6-DkcnpB-Y.js → timeline-definition-FHXFAJF6-wVOEmQwG.js} +1 -1
  56. package/dist/client/assets/{vennDiagram-L72KCM5P-BStcDGEf.js → vennDiagram-L72KCM5P-DMZiSJDk.js} +1 -1
  57. package/dist/client/assets/{wardleyDiagram-EHGQE667-Cj4CQn1M.js → wardleyDiagram-EHGQE667-Dx5Aob4Y.js} +1 -1
  58. package/dist/client/assets/{xychartDiagram-FW5EYKEG-_uBCjai8.js → xychartDiagram-FW5EYKEG-BqMhkhmW.js} +1 -1
  59. package/dist/client/index.html +1 -1
  60. package/dist/client/sw.js +1 -1
  61. package/dist/server/agent/autoTitle.js +16 -36
  62. package/dist/server/agent/installers.js +98 -15
  63. package/dist/server/agent/pluginPackages.js +289 -0
  64. package/dist/server/agent/plugins.js +164 -15
  65. package/dist/server/agent/registry.js +1 -1
  66. package/dist/server/agent/session.js +10 -1
  67. package/dist/server/agent/titleNamerCatalog.js +131 -2
  68. package/dist/server/agentHook.js +47 -0
  69. package/dist/server/cli.js +224 -13
  70. package/dist/server/entry.js +11 -2
  71. package/dist/server/routes/terminal.js +267 -37
  72. package/dist/server/utils/csrfProtection.js +4 -1
  73. package/dist/server/utils/settings.js +18 -5
  74. package/package.json +1 -1
  75. package/runtime-manifest.json +2 -2
  76. package/dist/client/assets/channel-BotxtiNE.js +0 -1
  77. package/dist/client/assets/classDiagram-OUVF2IWQ-CLWlbTSC.js +0 -1
  78. package/dist/client/assets/classDiagram-v2-EOCWNBFH-CLWlbTSC.js +0 -1
  79. package/dist/client/assets/index-ivnGnbVg.js +0 -451
  80. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-Dc-3HKTs.js +0 -1
  81. package/dist/client/assets/swimlanesDiagram-G3AALYLV-CI1Wlr0V.js +0 -8
@@ -14,22 +14,23 @@ 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, getAutoRenameAgentsSetting, setAutoRenameAgentsSetting, getAutoRenameNamerSetting, setAutoRenameNamerSetting, getAutoRenameModelsSetting, setAutoRenameModelsSetting, } from '../utils/settings.js';
17
+ import { normalizeLocalAccessName, getLocaleSetting, setLocaleSetting, getContextDraftHeightSetting, setContextDraftHeightSetting, getAutoRenameAgentsSetting, setAutoRenameAgentsSetting, getAutoRenameNamerSetting, setAutoRenameNamerSetting, getAutoRenameModelsSetting, setAutoRenameModelsSetting, getAutoRenameIntervalMinutesSetting, setAutoRenameIntervalMinutesSetting, } 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
22
  import { agentBySlug, buildResumeCommand, detectAgentFromCommand, listAgents, } from '../agent/registry.js';
23
- import { applyAgentEvent, defaultAgentSessionState, parseAgentEvent, } from '../agent/session.js';
23
+ import { applyAgentEvent, agentEventMatchesCurrentAgent, defaultAgentSessionState, parseAgentEvent, } from '../agent/session.js';
24
24
  import { listAllHookAgents, refreshStaleHooksAtLaunch, installHooksForSlug, uninstallHooksForSlug, } from '../agent/installers.js';
25
25
  import { loadPlugins, savePlugin, removePlugin, readPluginIcon, validateManifest, } from '../agent/plugins.js';
26
+ import { checkPluginPackageUpdate, commitPreparedPlugin, preparePluginPackage, } from '../agent/pluginPackages.js';
26
27
  import { clearPluginAgents, registerPluginAgents } from '../agent/registry.js';
27
28
  import { notifyAgentTransition, notifyTerminalExit } from '../notifications/pushService.js';
28
29
  import { setClientViewingSession } from '../notifications/pushViewers.js';
29
30
  import { getQuotaStatus, refreshQuota, startQuotaManager, } from '../quota/QuotaManager.js';
30
31
  import { TermdockAutoUpdateManager } from '../utils/autoUpdate.js';
31
- import { cleanTerminalContext, generateAgentTitle, isNewAgentSessionId, shouldReplaceAutoTitle } from '../agent/autoTitle.js';
32
- import { getTitleNamerCatalog } from '../agent/titleNamerCatalog.js';
32
+ import { AUTO_TITLE_MIN_CONTEXT_CHARS, cleanTerminalContext, generateAgentTitle, isNewAgentSessionId, isAutoTitleReevaluationDue, shouldReplaceAutoTitle, } from '../agent/autoTitle.js';
33
+ import { getTitleNamerCatalog, invalidateTitleNamerCatalog } from '../agent/titleNamerCatalog.js';
33
34
  const router = express.Router();
34
35
  const execFileAsync = promisify(execFile);
35
36
  const TERMDOCK_DIR = `${os.homedir()}/.termdock`;
@@ -2128,7 +2129,6 @@ function detectSessionAgent(session) {
2128
2129
  return null;
2129
2130
  }
2130
2131
  const AUTO_TITLE_CONTEXT_LIMIT = 24_000;
2131
- const AUTO_TITLE_REEVALUATE_MS = 60 * 60_000;
2132
2132
  const autoTitlePending = new Set();
2133
2133
  function appendAutoTitleContext(session, data) {
2134
2134
  session.autoTitleContext = (session.autoTitleContext + data).slice(-AUTO_TITLE_CONTEXT_LIMIT);
@@ -2155,10 +2155,14 @@ async function maybeAutoRenameSession(sessionId, agent, rawContext) {
2155
2155
  const record = globalSessionState.sessions.find((item) => item.backendSessionId === sessionId);
2156
2156
  if (!record || (record.customName === true && !record.autoTitle))
2157
2157
  return;
2158
- if (record.autoTitle && Date.now() - record.autoTitle.updatedAt < AUTO_TITLE_REEVALUATE_MS)
2158
+ if (record.autoTitle && !isAutoTitleReevaluationDue(record.autoTitle.updatedAt, getAutoRenameIntervalMinutesSetting()))
2159
2159
  return;
2160
2160
  const context = cleanTerminalContext(rawContext);
2161
- if (context.length < 120)
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)
2162
2166
  return;
2163
2167
  const contentHash = createHash('sha256').update(context).digest('hex');
2164
2168
  if (record.autoTitle?.contentHash === contentHash)
@@ -2389,6 +2393,8 @@ function applyAgentSignals(sessionId, session, events, notification) {
2389
2393
  let identityChanged = false;
2390
2394
  let completedTurnAgent = null;
2391
2395
  for (const event of events) {
2396
+ if (!agentEventMatchesCurrentAgent(session.agent, event))
2397
+ continue;
2392
2398
  // An event naming an agent brands the pane even when the process poll
2393
2399
  // can't see through a wrapper: identity via protocol.
2394
2400
  if (!session.agent && event.agent) {
@@ -2501,18 +2507,9 @@ function maybeRefreshGitStatusForAgent(sessionId, session) {
2501
2507
  session.gitAgentActivitySeen = activity;
2502
2508
  refreshGitStatus(sessionId, session);
2503
2509
  }
2504
- /**
2505
- * The command that resumes this pane's last agent conversation, or null.
2506
- * Live state wins; falls back to the persisted last-known record.
2507
- */
2508
- function resolveAgentResumeCommand(sessionId, session) {
2509
- const liveAgent = session.agent;
2510
- const live = session.agentSession;
2511
- if (liveAgent && live?.sessionId) {
2512
- const cmd = buildResumeCommand(liveAgent, live.sessionId, live.launchArgv);
2513
- if (cmd)
2514
- return cmd;
2515
- }
2510
+ function resolveAgentResumeTarget(sessionId, session) {
2511
+ // Resume always starts a new Agent process. Never offer it while the pane is
2512
+ // still inside an Agent TUI, even when that Agent reports a completed turn.
2516
2513
  if (!isPaneAtShellPrompt(session))
2517
2514
  return null;
2518
2515
  const record = globalSessionState.sessions.find((s) => s.backendSessionId === sessionId);
@@ -2522,7 +2519,66 @@ function resolveAgentResumeCommand(sessionId, session) {
2522
2519
  const agent = agentBySlug(persisted.slug);
2523
2520
  if (!agent)
2524
2521
  return null;
2525
- return buildResumeCommand(agent, persisted.sessionId, persisted.launchArgv);
2522
+ const command = buildResumeCommand(agent, persisted.sessionId, persisted.launchArgv);
2523
+ return command ? { slug: agent.slug, nativeSessionId: persisted.sessionId, command } : null;
2524
+ }
2525
+ function findActiveAgentResumeOwner(excludedBackendSessionId, target) {
2526
+ for (const [backendSessionId, candidate] of terminalSessions) {
2527
+ if (backendSessionId === excludedBackendSessionId || candidate.agent?.slug !== target.slug)
2528
+ continue;
2529
+ const record = globalSessionState.sessions.find((entry) => entry.backendSessionId === backendSessionId);
2530
+ const nativeSessionId = candidate.agentSession?.sessionId ?? record?.agentResume?.sessionId ?? null;
2531
+ if (nativeSessionId === target.nativeSessionId)
2532
+ return backendSessionId;
2533
+ }
2534
+ return null;
2535
+ }
2536
+ const codexSessionFileCache = new Map();
2537
+ async function findCodexSessionFile(nativeSessionId) {
2538
+ const cachedPath = codexSessionFileCache.get(nativeSessionId);
2539
+ if (cachedPath && fs.existsSync(cachedPath))
2540
+ return cachedPath;
2541
+ if (!/^[a-zA-Z0-9._-]{8,160}$/.test(nativeSessionId))
2542
+ return null;
2543
+ const root = path.join(os.homedir(), '.codex', 'sessions');
2544
+ const pending = [root];
2545
+ let visited = 0;
2546
+ while (pending.length > 0 && visited < 20_000) {
2547
+ const directory = pending.pop();
2548
+ let entries;
2549
+ try {
2550
+ entries = await fs.promises.readdir(directory, { withFileTypes: true });
2551
+ }
2552
+ catch {
2553
+ continue;
2554
+ }
2555
+ for (const entry of entries) {
2556
+ visited += 1;
2557
+ const candidate = path.join(directory, entry.name);
2558
+ if (entry.isDirectory())
2559
+ pending.push(candidate);
2560
+ else if (entry.isFile() && entry.name.endsWith(`-${nativeSessionId}.jsonl`)) {
2561
+ codexSessionFileCache.set(nativeSessionId, candidate);
2562
+ return candidate;
2563
+ }
2564
+ }
2565
+ }
2566
+ return null;
2567
+ }
2568
+ async function findExternalCodexWriter(target) {
2569
+ if (target.slug !== 'codex' || process.platform !== 'linux')
2570
+ return null;
2571
+ const sessionFile = await findCodexSessionFile(target.nativeSessionId);
2572
+ if (!sessionFile)
2573
+ return null;
2574
+ try {
2575
+ const { stdout } = await execFileAsync('lsof', ['-t', '--', sessionFile], { timeout: 2500, maxBuffer: 64 * 1024 });
2576
+ const pid = stdout.split(/\s+/).map(Number).find((value) => Number.isInteger(value) && value > 1);
2577
+ return pid ?? null;
2578
+ }
2579
+ catch {
2580
+ return null;
2581
+ }
2526
2582
  }
2527
2583
  /**
2528
2584
  * Deliver a prompt into an agent's PTY: bracketed paste (so multi-line
@@ -4372,6 +4428,7 @@ async function getSettingsPayload() {
4372
4428
  autoRenameAgents: getAutoRenameAgentsSetting(),
4373
4429
  autoRenameNamer: getAutoRenameNamerSetting(),
4374
4430
  autoRenameModels: getAutoRenameModelsSetting(),
4431
+ autoRenameIntervalMinutes: getAutoRenameIntervalMinutesSetting(),
4375
4432
  localAccess: {
4376
4433
  ...localAccess,
4377
4434
  interfaces,
@@ -4427,21 +4484,23 @@ router.put('/settings', async (req, res) => {
4427
4484
  const requested = body.autoRenameAgents
4428
4485
  .filter((slug) => typeof slug === 'string' && knownSlugs.has(slug));
4429
4486
  setAutoRenameAgentsSetting(requested);
4487
+ invalidateTitleNamerCatalog();
4430
4488
  }
4431
4489
  if (typeof body.autoRenameNamer === 'string') {
4432
- if (!['auto', 'codex', 'claude'].includes(body.autoRenameNamer)) {
4490
+ const available = new Set((await getTitleNamerCatalog()).filter((entry) => entry.available).map((entry) => entry.slug));
4491
+ if (body.autoRenameNamer !== 'auto' && !available.has(body.autoRenameNamer)) {
4433
4492
  res.status(400).json({ error: 'Unsupported automatic title agent', code: 'AUTO_RENAME_NAMER_INVALID' });
4434
4493
  return;
4435
4494
  }
4436
4495
  setAutoRenameNamerSetting(body.autoRenameNamer);
4496
+ invalidateTitleNamerCatalog();
4437
4497
  }
4438
4498
  if (body.autoRenameModels && typeof body.autoRenameModels === 'object') {
4439
4499
  const requested = body.autoRenameModels;
4440
4500
  const catalog = await getTitleNamerCatalog();
4441
4501
  const allowed = new Map(catalog.map((namer) => [namer.slug, new Set(namer.models.map((model) => model.id))]));
4442
4502
  const models = {};
4443
- for (const slug of ['codex', 'claude']) {
4444
- const model = requested[slug];
4503
+ for (const [slug, model] of Object.entries(requested)) {
4445
4504
  if (model === undefined || model === '')
4446
4505
  continue;
4447
4506
  if (typeof model !== 'string' || !allowed.get(slug)?.has(model)) {
@@ -4452,6 +4511,12 @@ router.put('/settings', async (req, res) => {
4452
4511
  }
4453
4512
  setAutoRenameModelsSetting(models);
4454
4513
  }
4514
+ if (typeof body.autoRenameIntervalMinutes === 'number'
4515
+ && Number.isInteger(body.autoRenameIntervalMinutes)
4516
+ && body.autoRenameIntervalMinutes >= 5
4517
+ && body.autoRenameIntervalMinutes <= 1440) {
4518
+ setAutoRenameIntervalMinutesSetting(body.autoRenameIntervalMinutes);
4519
+ }
4455
4520
  if (body.localAccess && typeof body.localAccess === 'object') {
4456
4521
  const localAccessBody = body.localAccess;
4457
4522
  if (localAccessBody.reset === true) {
@@ -4494,8 +4559,8 @@ router.put('/context-draft', (req, res) => {
4494
4559
  res.json(doc);
4495
4560
  });
4496
4561
  // ── Agent hooks API (rich status channel installers, built-in + plugin) ──
4497
- router.get('/agent-hooks', (_req, res) => {
4498
- res.json({ agents: listAllHookAgents() });
4562
+ router.get('/agent-hooks', async (_req, res) => {
4563
+ res.json({ agents: await listAllHookAgents() });
4499
4564
  });
4500
4565
  router.get('/agent-launchers', async (_req, res) => {
4501
4566
  const pathEntries = buildAugmentedPath().split(path.delimiter).filter(Boolean);
@@ -4601,11 +4666,134 @@ router.get('/agent-plugins', (_req, res) => {
4601
4666
  iconMode: p.manifest.iconMode ?? 'mask',
4602
4667
  hasHooks: p.manifest.hooks !== undefined,
4603
4668
  hasResume: p.manifest.resume !== undefined,
4669
+ hasTitleNamer: p.manifest.titleNamer !== undefined,
4604
4670
  hasIcon: p.iconPath !== null,
4671
+ sourceType: p.source?.type ?? 'manifest',
4672
+ source: p.source?.source ?? null,
4673
+ revision: p.source?.revision ?? null,
4674
+ latestRevision: p.source?.latestRevision ?? null,
4675
+ checkedAt: p.source?.checkedAt ?? null,
4676
+ updatedAt: p.source?.updatedAt ?? null,
4677
+ updateSupported: Boolean(p.source?.source && p.source.type !== 'manifest'),
4678
+ updateAvailable: Boolean(p.source?.revision && p.source.latestRevision && p.source.revision !== p.source.latestRevision),
4605
4679
  })),
4606
4680
  errors: errors.map((e) => ({ slug: e.slug, errors: e.errors, code: e.code, migration: e.migration })),
4607
4681
  });
4608
4682
  });
4683
+ function reloadAgentPlugins() {
4684
+ invalidateTitleNamerCatalog();
4685
+ clearPluginAgents();
4686
+ registerPluginAgents(loadPlugins().plugins);
4687
+ }
4688
+ router.post('/agent-plugins/install', async (req, res) => {
4689
+ const source = typeof req.body?.source === 'string' ? req.body.source.trim() : '';
4690
+ if (!source) {
4691
+ res.status(400).json({ error: 'Plugin source is required', code: 'AGENT_PLUGIN_SOURCE_REQUIRED' });
4692
+ return;
4693
+ }
4694
+ try {
4695
+ const prepared = await preparePluginPackage(source);
4696
+ const dir = await commitPreparedPlugin(prepared, false);
4697
+ reloadAgentPlugins();
4698
+ res.json({ slug: prepared.manifest.slug, dir, state: 'installed', source: prepared.metadata });
4699
+ }
4700
+ catch (error) {
4701
+ const detail = error;
4702
+ res.status(400).json({
4703
+ error: detail.message,
4704
+ code: detail.code ?? 'AGENT_PLUGIN_INSTALL_FAILED',
4705
+ migration: detail.migration,
4706
+ });
4707
+ }
4708
+ });
4709
+ router.post('/agent-plugins/:slug/check-update', async (req, res) => {
4710
+ const plugin = loadPlugins().plugins.find((entry) => entry.manifest.slug === req.params.slug);
4711
+ if (!plugin) {
4712
+ res.status(404).json({ error: `Plugin "${req.params.slug}" not found` });
4713
+ return;
4714
+ }
4715
+ try {
4716
+ res.json(await checkPluginPackageUpdate(plugin));
4717
+ }
4718
+ catch (error) {
4719
+ res.status(400).json({ error: error.message, code: 'AGENT_PLUGIN_UPDATE_CHECK_FAILED' });
4720
+ }
4721
+ });
4722
+ router.post('/agent-plugins/:slug/update', async (req, res) => {
4723
+ const slug = req.params.slug;
4724
+ const plugin = loadPlugins().plugins.find((entry) => entry.manifest.slug === slug);
4725
+ if (!plugin) {
4726
+ res.status(404).json({ error: `Plugin "${slug}" not found` });
4727
+ return;
4728
+ }
4729
+ if (!plugin.source?.source || plugin.source.type === 'manifest') {
4730
+ res.status(400).json({ error: 'This plugin has no update source', code: 'AGENT_PLUGIN_UPDATE_UNSUPPORTED' });
4731
+ return;
4732
+ }
4733
+ let prepared = null;
4734
+ let oldHooksRemoved = false;
4735
+ let committed = false;
4736
+ try {
4737
+ // Download and validate the complete replacement before touching the live
4738
+ // package or its currently installed hooks.
4739
+ prepared = await preparePluginPackage(plugin.source.source, slug);
4740
+ const hooksChanged = JSON.stringify(plugin.manifest.hooks ?? null) !== JSON.stringify(prepared.manifest.hooks ?? null);
4741
+ const titleCapabilityChanged = JSON.stringify(plugin.manifest.titleNamer ?? null) !== JSON.stringify(prepared.manifest.titleNamer ?? null);
4742
+ const hookState = (await listAllHookAgents()).find((entry) => entry.slug === slug)?.state;
4743
+ const restoreHooks = hookState === 'installed' || hookState === 'outdated';
4744
+ if (restoreHooks && plugin.manifest.hooks) {
4745
+ await uninstallHooksForSlug(slug);
4746
+ oldHooksRemoved = true;
4747
+ }
4748
+ const dir = await commitPreparedPlugin(prepared, true);
4749
+ prepared = null;
4750
+ committed = true;
4751
+ reloadAgentPlugins();
4752
+ let hookWarning = null;
4753
+ if (restoreHooks && hooksChanged) {
4754
+ hookWarning = 'Hook declarations changed during update. Review them, then install hooks again.';
4755
+ }
4756
+ else if (restoreHooks) {
4757
+ try {
4758
+ await installHooksForSlug(slug);
4759
+ }
4760
+ catch (error) {
4761
+ hookWarning = error.message;
4762
+ }
4763
+ }
4764
+ let titleWarning = null;
4765
+ if (titleCapabilityChanged) {
4766
+ setAutoRenameAgentsSetting(getAutoRenameAgentsSetting().filter((entry) => entry !== slug));
4767
+ if (getAutoRenameNamerSetting() === slug)
4768
+ setAutoRenameNamerSetting('auto');
4769
+ const models = getAutoRenameModelsSetting();
4770
+ delete models[slug];
4771
+ setAutoRenameModelsSetting(models);
4772
+ invalidateTitleNamerCatalog();
4773
+ titleWarning = 'Title command declarations changed during update. Automatic titles for this plugin were disabled; review and enable them again.';
4774
+ }
4775
+ res.json({
4776
+ slug,
4777
+ dir,
4778
+ state: 'updated',
4779
+ source: loadPlugins().plugins.find((entry) => entry.manifest.slug === slug)?.source,
4780
+ hookWarning,
4781
+ titleWarning,
4782
+ });
4783
+ }
4784
+ catch (error) {
4785
+ if (prepared)
4786
+ await prepared.cleanup().catch(() => undefined);
4787
+ if (oldHooksRemoved && !committed)
4788
+ await installHooksForSlug(slug).catch(() => undefined);
4789
+ const detail = error;
4790
+ res.status(400).json({
4791
+ error: detail.message,
4792
+ code: detail.code ?? 'AGENT_PLUGIN_UPDATE_FAILED',
4793
+ migration: detail.migration,
4794
+ });
4795
+ }
4796
+ });
4609
4797
  router.post('/agent-plugins', async (req, res) => {
4610
4798
  const manifest = req.body;
4611
4799
  if (!manifest || typeof manifest !== 'object') {
@@ -4631,9 +4819,7 @@ router.post('/agent-plugins', async (req, res) => {
4631
4819
  return;
4632
4820
  }
4633
4821
  const dir = savePlugin(validation.manifest);
4634
- clearPluginAgents();
4635
- const loaded = loadPlugins();
4636
- registerPluginAgents(loaded.plugins);
4822
+ reloadAgentPlugins();
4637
4823
  res.json({ slug: validation.manifest.slug, dir, state: 'created' });
4638
4824
  }
4639
4825
  catch (error) {
@@ -4643,13 +4829,32 @@ router.post('/agent-plugins', async (req, res) => {
4643
4829
  router.delete('/agent-plugins/:slug', async (req, res) => {
4644
4830
  const { slug } = req.params;
4645
4831
  try {
4832
+ const plugin = loadPlugins().plugins.find((entry) => entry.manifest.slug === slug);
4833
+ if (!plugin) {
4834
+ res.status(404).json({ error: `Plugin "${slug}" not found` });
4835
+ return;
4836
+ }
4837
+ // Uninstall while the manifest is still present: after deletion we no
4838
+ // longer know the target file or event shape and would orphan commands in
4839
+ // the Agent's config.
4840
+ if (plugin.manifest.hooks)
4841
+ await uninstallHooksForSlug(slug);
4646
4842
  removePlugin(slug);
4843
+ invalidateTitleNamerCatalog();
4844
+ setAutoRenameAgentsSetting(getAutoRenameAgentsSetting().filter((entry) => entry !== slug));
4845
+ if (getAutoRenameNamerSetting() === slug)
4846
+ setAutoRenameNamerSetting('auto');
4847
+ const models = getAutoRenameModelsSetting();
4848
+ if (models[slug]) {
4849
+ delete models[slug];
4850
+ setAutoRenameModelsSetting(models);
4851
+ }
4647
4852
  clearPluginAgents();
4648
4853
  registerPluginAgents(loadPlugins().plugins);
4649
4854
  res.json({ slug, state: 'removed' });
4650
4855
  }
4651
4856
  catch (error) {
4652
- res.status(404).json({ error: error.message });
4857
+ res.status(400).json({ error: error.message });
4653
4858
  }
4654
4859
  });
4655
4860
  router.get('/agent-plugin-icon/:slug', (req, res) => {
@@ -5099,7 +5304,7 @@ router.post('/:sessionId/input', express.text({ type: '*/*' }), (req, res) => {
5099
5304
  * bracketed paste. Refused while an agent turn is in flight — pasting a
5100
5305
  * resume into a busy TUI would corrupt its input.
5101
5306
  */
5102
- router.post('/:sessionId/agent-resume', (req, res) => {
5307
+ router.post('/:sessionId/agent-resume', async (req, res) => {
5103
5308
  const { sessionId } = req.params;
5104
5309
  const session = terminalSessions.get(sessionId);
5105
5310
  if (!session) {
@@ -5109,14 +5314,30 @@ router.post('/:sessionId/agent-resume', (req, res) => {
5109
5314
  if (session.agent && live && (live.status === 'working' || live.status === 'waiting')) {
5110
5315
  return res.status(409).json({ error: 'agent is busy; wait for the turn to finish' });
5111
5316
  }
5112
- const command = resolveAgentResumeCommand(sessionId, session);
5113
- if (!command) {
5317
+ const target = resolveAgentResumeTarget(sessionId, session);
5318
+ if (!target) {
5114
5319
  return res.status(404).json({ error: 'no resumable agent session for this pane' });
5115
5320
  }
5321
+ const activeOwner = findActiveAgentResumeOwner(sessionId, target);
5322
+ if (activeOwner) {
5323
+ return res.status(409).json({
5324
+ error: 'this agent session is already open in another terminal',
5325
+ code: 'AGENT_SESSION_ACTIVE_ELSEWHERE',
5326
+ activeBackendSessionId: activeOwner,
5327
+ });
5328
+ }
5329
+ const activeWriterPid = await findExternalCodexWriter(target);
5330
+ if (activeWriterPid) {
5331
+ return res.status(409).json({
5332
+ error: 'this Codex thread already has an active writer',
5333
+ code: 'AGENT_SESSION_ACTIVE_ELSEWHERE',
5334
+ activeWriterPid,
5335
+ });
5336
+ }
5116
5337
  try {
5117
- session.ptyProcess.write(buildBracketedSubmitBytes(command));
5338
+ session.ptyProcess.write(buildBracketedSubmitBytes(target.command));
5118
5339
  session.lastActivity = Date.now();
5119
- res.json({ success: true, command });
5340
+ res.json({ success: true, command: target.command });
5120
5341
  }
5121
5342
  catch (error) {
5122
5343
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -5124,16 +5345,25 @@ router.post('/:sessionId/agent-resume', (req, res) => {
5124
5345
  }
5125
5346
  });
5126
5347
  /** Whether this pane has a resumable agent conversation (for the UI affordance). */
5127
- router.get('/:sessionId/agent-resume', (req, res) => {
5348
+ router.get('/:sessionId/agent-resume', async (req, res) => {
5128
5349
  const { sessionId } = req.params;
5129
5350
  const session = terminalSessions.get(sessionId);
5130
5351
  if (!session) {
5131
5352
  return res.status(404).json({ error: 'Terminal session not found' });
5132
5353
  }
5133
- const command = resolveAgentResumeCommand(sessionId, session);
5354
+ const target = resolveAgentResumeTarget(sessionId, session);
5134
5355
  const busy = !!(session.agent && session.agentSession
5135
5356
  && (session.agentSession.status === 'working' || session.agentSession.status === 'waiting'));
5136
- res.json({ available: command !== null, command, busy });
5357
+ const activeBackendSessionId = target ? findActiveAgentResumeOwner(sessionId, target) : null;
5358
+ const activeWriterPid = target && !activeBackendSessionId ? await findExternalCodexWriter(target) : null;
5359
+ res.json({
5360
+ available: target !== null && activeBackendSessionId === null && activeWriterPid === null,
5361
+ command: target?.command ?? null,
5362
+ busy,
5363
+ conflict: activeBackendSessionId || activeWriterPid ? 'active-elsewhere' : null,
5364
+ activeBackendSessionId,
5365
+ activeWriterPid,
5366
+ });
5137
5367
  });
5138
5368
  router.post('/:sessionId/resize', async (req, res) => {
5139
5369
  const { sessionId } = req.params;
@@ -62,8 +62,11 @@ export class CsrfProtection {
62
62
  /**
63
63
  * Express中间件:验证CSRF令牌
64
64
  */
65
- verifyMiddleware() {
65
+ verifyMiddleware(options) {
66
66
  return (req, res, next) => {
67
+ if (options?.bypass?.(req)) {
68
+ return next();
69
+ }
67
70
  // 跳过安全检查和GET/HEAD/OPTIONS请求
68
71
  const safeMethods = ['GET', 'HEAD', 'OPTIONS'];
69
72
  if (safeMethods.includes(req.method)) {
@@ -71,15 +71,20 @@ function normalizeSettings(value) {
71
71
  locale: typeof raw.locale === 'string' && raw.locale === 'zh' ? 'zh' : 'en',
72
72
  contextDraftHeight: normalizeContextDraftHeight(raw.contextDraftHeight),
73
73
  autoRenameAgents,
74
- autoRenameNamer: raw.autoRenameNamer === 'codex' || raw.autoRenameNamer === 'claude'
75
- ? raw.autoRenameNamer
76
- : 'auto',
74
+ autoRenameNamer: typeof raw.autoRenameNamer === 'string' && /^(?:auto|[a-z][a-z0-9-]{0,39})$/.test(raw.autoRenameNamer)
75
+ ? raw.autoRenameNamer : 'auto',
77
76
  autoRenameModels: raw.autoRenameModels && typeof raw.autoRenameModels === 'object'
78
77
  ? Object.fromEntries(Object.entries(raw.autoRenameModels)
79
- .filter((entry) => (['codex', 'claude'].includes(entry[0])
78
+ .filter((entry) => (/^[a-z][a-z0-9-]{0,39}$/.test(entry[0])
80
79
  && typeof entry[1] === 'string'
81
80
  && /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,119}$/.test(entry[1]))))
82
81
  : {},
82
+ autoRenameIntervalMinutes: typeof raw.autoRenameIntervalMinutes === 'number'
83
+ && Number.isInteger(raw.autoRenameIntervalMinutes)
84
+ && raw.autoRenameIntervalMinutes >= 5
85
+ && raw.autoRenameIntervalMinutes <= 1440
86
+ ? raw.autoRenameIntervalMinutes
87
+ : 10,
83
88
  updatedAt: typeof raw.updatedAt === 'number' ? raw.updatedAt : Date.now(),
84
89
  };
85
90
  }
@@ -219,10 +224,18 @@ export function setAutoRenameNamerSetting(namer) {
219
224
  export function getAutoRenameModelsSetting() {
220
225
  return { ...loadSettings().autoRenameModels };
221
226
  }
227
+ export function getAutoRenameIntervalMinutesSetting() {
228
+ return loadSettings().autoRenameIntervalMinutes;
229
+ }
230
+ export function setAutoRenameIntervalMinutesSetting(minutes) {
231
+ return updateSettings((settings) => {
232
+ settings.autoRenameIntervalMinutes = Math.max(5, Math.min(1440, Math.round(minutes)));
233
+ });
234
+ }
222
235
  export function setAutoRenameModelsSetting(models) {
223
236
  return updateSettings((settings) => {
224
237
  settings.autoRenameModels = Object.fromEntries(Object.entries(models)
225
- .filter(([slug, model]) => (['codex', 'claude'].includes(slug)
238
+ .filter(([slug, model]) => (/^[a-z][a-z0-9-]{0,39}$/.test(slug)
226
239
  && /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,119}$/.test(model))));
227
240
  });
228
241
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termdock",
3
- "version": "1.4.70",
3
+ "version": "1.4.72",
4
4
  "description": "A complete web-based terminal application with modern UI",
5
5
  "type": "module",
6
6
  "main": ".desktop-dist/main.js",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "packageName": "termdock",
4
- "version": "1.4.70",
4
+ "version": "1.4.72",
5
5
  "runtimeProtocolVersion": 1,
6
6
  "minimumDesktopVersion": "1.4.46",
7
7
  "nodeMajor": 22,
8
8
  "dependencyHash": "sha256-HxC6p3b4kFesefw+vmNYJtPtdIKJaZR1jFNb7DRJqjU=",
9
- "serverBundleHash": "sha256-EUadVIzJckC7mu5Rh1kKr0HMuFj+let9WDY0LHnOFpA=",
9
+ "serverBundleHash": "sha256-4Ti+r7BiQtaMfPn2SORjiaLhcxgBT7xkGbas+Dsh6Zo=",
10
10
  "entrypoint": "dist/server/cli.js",
11
11
  "clientEntrypoint": "dist/client/index.html"
12
12
  }
@@ -1 +0,0 @@
1
- import{U as a,C as n}from"./mermaid.core-eJs7Ouvw.js";const t=(r,o)=>a.lang.round(n.parse(r)[o]);export{t as c};
@@ -1 +0,0 @@
1
- import{s as a,a as s,c as t,C as e}from"./chunk-V7JOEXUC-BK4_RLz-.js";import{b as i}from"./mermaid.core-eJs7Ouvw.js";import"./chunk-5VM5RSS4-BfBshn11.js";import"./chunk-XXDRQBXY-BolvPLRz.js";import"./chunk-VR4S4FIN-Kj5_ycjB.js";import"./chunk-32BRIVSS-CXrK07sW.js";import"./purify.es-CC4Brkr_.js";import"./index-ivnGnbVg.js";import"./react-XDCjal2T.js";import"./dnd-BCFAPC0L.js";import"./terminal-C7X98Amf.js";var g={parser:t,get db(){return new e},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{g as diagram};
@@ -1 +0,0 @@
1
- import{s as a,a as s,c as t,C as e}from"./chunk-V7JOEXUC-BK4_RLz-.js";import{b as i}from"./mermaid.core-eJs7Ouvw.js";import"./chunk-5VM5RSS4-BfBshn11.js";import"./chunk-XXDRQBXY-BolvPLRz.js";import"./chunk-VR4S4FIN-Kj5_ycjB.js";import"./chunk-32BRIVSS-CXrK07sW.js";import"./purify.es-CC4Brkr_.js";import"./index-ivnGnbVg.js";import"./react-XDCjal2T.js";import"./dnd-BCFAPC0L.js";import"./terminal-C7X98Amf.js";var g={parser:t,get db(){return new e},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{g as diagram};