termdock 1.4.71 → 1.4.73

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 (77) hide show
  1. package/README.md +59 -0
  2. package/dist/client/assets/{ModelPreview-CG6vLwyw.js → ModelPreview-BYS46cJp.js} +1 -1
  3. package/dist/client/assets/{abnfDiagram-VRR7QNED-CrXEZLWX.js → abnfDiagram-VRR7QNED-Df5iizBM.js} +1 -1
  4. package/dist/client/assets/{arc-DbKGR7Ex.js → arc-BRLIrHaj.js} +1 -1
  5. package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-PF5BoUbI.js → architectureDiagram-ZJ3FMSHR-BmIKhrrq.js} +1 -1
  6. package/dist/client/assets/{blockDiagram-677ZJIJ3-D1gRzVWB.js → blockDiagram-677ZJIJ3-PBtq2qLG.js} +1 -1
  7. package/dist/client/assets/{c4Diagram-LMCZKHZV-CwvOLync.js → c4Diagram-LMCZKHZV-6pevynP4.js} +1 -1
  8. package/dist/client/assets/channel-CfecnPVa.js +1 -0
  9. package/dist/client/assets/{chunk-2Q5K7J3B-BeRdP-xP.js → chunk-2Q5K7J3B-78EZViva.js} +1 -1
  10. package/dist/client/assets/{chunk-32BRIVSS-CrqR5rvl.js → chunk-32BRIVSS-CekczK5l.js} +1 -1
  11. package/dist/client/assets/{chunk-5VM5RSS4-CjbUr-ko.js → chunk-5VM5RSS4-fMhlGbVA.js} +1 -1
  12. package/dist/client/assets/{chunk-EX3LRPZG-BVEk3Wma.js → chunk-EX3LRPZG-DOBewV4Z.js} +1 -1
  13. package/dist/client/assets/{chunk-JWPE2WC7-WitJe8kR.js → chunk-JWPE2WC7-rtk-mGC7.js} +1 -1
  14. package/dist/client/assets/{chunk-MOJQB5TN-BCewlLYk.js → chunk-MOJQB5TN-CWUZocq_.js} +1 -1
  15. package/dist/client/assets/{chunk-RYQCIY6F-CoP0AA4G.js → chunk-RYQCIY6F-DcHAjeyV.js} +1 -1
  16. package/dist/client/assets/{chunk-V7JOEXUC-CSZrnmp_.js → chunk-V7JOEXUC-C2paDSKz.js} +1 -1
  17. package/dist/client/assets/{chunk-VR4S4FIN-BB-7zz-9.js → chunk-VR4S4FIN-Cq0ZrFUq.js} +1 -1
  18. package/dist/client/assets/{chunk-XXDRQBXY-CcZZlCik.js → chunk-XXDRQBXY-BboySYmP.js} +1 -1
  19. package/dist/client/assets/classDiagram-OUVF2IWQ-CFpMarVm.js +1 -0
  20. package/dist/client/assets/classDiagram-v2-EOCWNBFH-CFpMarVm.js +1 -0
  21. package/dist/client/assets/{cose-bilkent-JH36ORCC-DcmzUDwh.js → cose-bilkent-JH36ORCC-CMnHIr6y.js} +1 -1
  22. package/dist/client/assets/{cynefin-VYW2F7L2-C1XlDVoJ.js → cynefin-VYW2F7L2-BpOtLuYh.js} +1 -1
  23. package/dist/client/assets/{cynefinDiagram-TSTJHNR4-zY6Aet2S.js → cynefinDiagram-TSTJHNR4-CmrMqA7z.js} +1 -1
  24. package/dist/client/assets/{dagre-VKFMJZFB-DqGTi6zR.js → dagre-VKFMJZFB-iXOV-rm7.js} +1 -1
  25. package/dist/client/assets/{diagram-FQU43EPY-DStcsU7S.js → diagram-FQU43EPY-D5KagnmX.js} +1 -1
  26. package/dist/client/assets/{diagram-G47NLZAW-Cs6IMNip.js → diagram-G47NLZAW-CglgkfyY.js} +1 -1
  27. package/dist/client/assets/{diagram-NH7WQ7WH-yA2bpWN7.js → diagram-NH7WQ7WH-Dos-y1pl.js} +1 -1
  28. package/dist/client/assets/{diagram-OA4YK3LP-Dzf9M0PI.js → diagram-OA4YK3LP-Cw6yJqFX.js} +1 -1
  29. package/dist/client/assets/{diagram-WEI45ONY-D75lpQKA.js → diagram-WEI45ONY-C_MHHYRA.js} +1 -1
  30. package/dist/client/assets/{ebnfDiagram-CCIWWBDH-CkQvIpWy.js → ebnfDiagram-CCIWWBDH-BB7x8X2l.js} +1 -1
  31. package/dist/client/assets/{erDiagram-Q63AITRT-Ca00Vjxz.js → erDiagram-Q63AITRT-BznrrU3P.js} +1 -1
  32. package/dist/client/assets/{flowDiagram-23GEKE2U-CGrnSTEp.js → flowDiagram-23GEKE2U-CMzPvwGa.js} +1 -1
  33. package/dist/client/assets/{ganttDiagram-NO4QXBWP-0SHfOIP4.js → ganttDiagram-NO4QXBWP-Dc9JCHv3.js} +1 -1
  34. package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-BeigYWvs.js → gitGraphDiagram-IHSO6WYX-WlPpmZQm.js} +1 -1
  35. package/dist/client/assets/index-wX3Y6Ks7.js +451 -0
  36. package/dist/client/assets/{infoDiagram-FWYZ7A6U-CiY1-xjN.js → infoDiagram-FWYZ7A6U-DC6PBJiN.js} +1 -1
  37. package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-CmtDjUm_.js → ishikawaDiagram-FXEZZL3T-DPfxPu0W.js} +1 -1
  38. package/dist/client/assets/{journeyDiagram-5HDEW3XC-B7qOr9LY.js → journeyDiagram-5HDEW3XC-aA2ovr_f.js} +1 -1
  39. package/dist/client/assets/{kanban-definition-HUTT4EX6-DO2P2Um3.js → kanban-definition-HUTT4EX6-_T30f7Or.js} +1 -1
  40. package/dist/client/assets/{linear-B832hJt0.js → linear-DRZcO7_m.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-CsAzdiCU.js → mermaid.core-BGXjYfdb.js} +4 -4
  42. package/dist/client/assets/{mindmap-definition-LN4V7U3C-HI__vXhQ.js → mindmap-definition-LN4V7U3C-Bv0lhpqs.js} +1 -1
  43. package/dist/client/assets/{pegDiagram-2B236MQR--rZIwjXq.js → pegDiagram-2B236MQR-D-EIIOZf.js} +1 -1
  44. package/dist/client/assets/{pieDiagram-ENE6RG2P-D9jPf5x9.js → pieDiagram-ENE6RG2P-CATAcPgl.js} +1 -1
  45. package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-DTCP5ji2.js → quadrantDiagram-ABIIQ3AL-2tglHVSk.js} +1 -1
  46. package/dist/client/assets/{railroadDiagram-RFXS5EU6-D2Gspvrl.js → railroadDiagram-RFXS5EU6-BMKC-e8R.js} +1 -1
  47. package/dist/client/assets/{requirementDiagram-TGXJPOKE-DEJC02IW.js → requirementDiagram-TGXJPOKE-B5J5_wFW.js} +1 -1
  48. package/dist/client/assets/{sankeyDiagram-HTMAVEWB-opOhKPap.js → sankeyDiagram-HTMAVEWB-Djlm71ae.js} +1 -1
  49. package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-_sz2CRAG.js → sequenceDiagram-DBY2YBRQ-BtR8OPSJ.js} +1 -1
  50. package/dist/client/assets/{sizeCapture-X5ZJPWSS-D0Q2QkYF.js → sizeCapture-X5ZJPWSS-CDelD9h_.js} +1 -1
  51. package/dist/client/assets/{stateDiagram-2N3HPSRC-VgeSQTtW.js → stateDiagram-2N3HPSRC-C50kGru1.js} +1 -1
  52. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-gIr1L4WZ.js +1 -0
  53. package/dist/client/assets/{swimlanes-5IMT3BWC-ClvBvSl0.js → swimlanes-5IMT3BWC-COHxl86-.js} +2 -2
  54. package/dist/client/assets/swimlanesDiagram-G3AALYLV-COE4p0kX.js +8 -0
  55. package/dist/client/assets/{timeline-definition-FHXFAJF6-BXNsrECV.js → timeline-definition-FHXFAJF6-br-VI8Fd.js} +1 -1
  56. package/dist/client/assets/{vennDiagram-L72KCM5P-BgwiqZd-.js → vennDiagram-L72KCM5P-zwMtEv81.js} +1 -1
  57. package/dist/client/assets/{wardleyDiagram-EHGQE667-DiqN5ERx.js → wardleyDiagram-EHGQE667-_C8v4nnr.js} +1 -1
  58. package/dist/client/assets/{xychartDiagram-FW5EYKEG-DQjFVqC6.js → xychartDiagram-FW5EYKEG-CSU2R6pt.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 +3 -0
  62. package/dist/server/agent/pluginPackages.js +289 -0
  63. package/dist/server/agent/plugins.js +139 -20
  64. package/dist/server/agent/titleNamerCatalog.js +235 -40
  65. package/dist/server/cli.js +277 -17
  66. package/dist/server/entry.js +11 -2
  67. package/dist/server/routes/terminal.js +241 -30
  68. package/dist/server/utils/csrfProtection.js +4 -1
  69. package/dist/server/utils/settings.js +14 -0
  70. package/package.json +1 -1
  71. package/runtime-manifest.json +2 -2
  72. package/dist/client/assets/channel-JTEuKVy6.js +0 -1
  73. package/dist/client/assets/classDiagram-OUVF2IWQ-BiFJB_5s.js +0 -1
  74. package/dist/client/assets/classDiagram-v2-EOCWNBFH-BiFJB_5s.js +0 -1
  75. package/dist/client/assets/index-D5PKdbhW.js +0 -451
  76. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-BVh2FRO-.js +0 -1
  77. package/dist/client/assets/swimlanesDiagram-G3AALYLV-C17V-kgm.js +0 -8
@@ -14,7 +14,7 @@ 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';
@@ -23,13 +23,14 @@ import { agentBySlug, buildResumeCommand, detectAgentFromCommand, listAgents, }
23
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 { AUTO_TITLE_MIN_CONTEXT_CHARS, cleanTerminalContext, generateAgentTitle, isNewAgentSessionId, shouldReplaceAutoTitle, } from '../agent/autoTitle.js';
32
- import { getTitleNamerCatalog, invalidateTitleNamerCatalog } from '../agent/titleNamerCatalog.js';
32
+ import { AUTO_TITLE_MIN_CONTEXT_CHARS, cleanTerminalContext, generateAgentTitle, isNewAgentSessionId, isAutoTitleReevaluationDue, shouldReplaceAutoTitle, } from '../agent/autoTitle.js';
33
+ import { getTitleNamerCatalog, invalidateTitleNamerCatalog, probePluginTitleNamer } 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,7 +2155,7 @@ 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
2161
  // A new session gets one cheap attempt after its first real exchange. Short
@@ -2507,18 +2507,9 @@ function maybeRefreshGitStatusForAgent(sessionId, session) {
2507
2507
  session.gitAgentActivitySeen = activity;
2508
2508
  refreshGitStatus(sessionId, session);
2509
2509
  }
2510
- /**
2511
- * The command that resumes this pane's last agent conversation, or null.
2512
- * Live state wins; falls back to the persisted last-known record.
2513
- */
2514
- function resolveAgentResumeCommand(sessionId, session) {
2515
- const liveAgent = session.agent;
2516
- const live = session.agentSession;
2517
- if (liveAgent && live?.sessionId) {
2518
- const cmd = buildResumeCommand(liveAgent, live.sessionId, live.launchArgv);
2519
- if (cmd)
2520
- return cmd;
2521
- }
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.
2522
2513
  if (!isPaneAtShellPrompt(session))
2523
2514
  return null;
2524
2515
  const record = globalSessionState.sessions.find((s) => s.backendSessionId === sessionId);
@@ -2528,7 +2519,66 @@ function resolveAgentResumeCommand(sessionId, session) {
2528
2519
  const agent = agentBySlug(persisted.slug);
2529
2520
  if (!agent)
2530
2521
  return null;
2531
- 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
+ }
2532
2582
  }
2533
2583
  /**
2534
2584
  * Deliver a prompt into an agent's PTY: bracketed paste (so multi-line
@@ -4378,6 +4428,7 @@ async function getSettingsPayload() {
4378
4428
  autoRenameAgents: getAutoRenameAgentsSetting(),
4379
4429
  autoRenameNamer: getAutoRenameNamerSetting(),
4380
4430
  autoRenameModels: getAutoRenameModelsSetting(),
4431
+ autoRenameIntervalMinutes: getAutoRenameIntervalMinutesSetting(),
4381
4432
  localAccess: {
4382
4433
  ...localAccess,
4383
4434
  interfaces,
@@ -4433,6 +4484,7 @@ router.put('/settings', async (req, res) => {
4433
4484
  const requested = body.autoRenameAgents
4434
4485
  .filter((slug) => typeof slug === 'string' && knownSlugs.has(slug));
4435
4486
  setAutoRenameAgentsSetting(requested);
4487
+ invalidateTitleNamerCatalog();
4436
4488
  }
4437
4489
  if (typeof body.autoRenameNamer === 'string') {
4438
4490
  const available = new Set((await getTitleNamerCatalog()).filter((entry) => entry.available).map((entry) => entry.slug));
@@ -4441,6 +4493,7 @@ router.put('/settings', async (req, res) => {
4441
4493
  return;
4442
4494
  }
4443
4495
  setAutoRenameNamerSetting(body.autoRenameNamer);
4496
+ invalidateTitleNamerCatalog();
4444
4497
  }
4445
4498
  if (body.autoRenameModels && typeof body.autoRenameModels === 'object') {
4446
4499
  const requested = body.autoRenameModels;
@@ -4458,6 +4511,12 @@ router.put('/settings', async (req, res) => {
4458
4511
  }
4459
4512
  setAutoRenameModelsSetting(models);
4460
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
+ }
4461
4520
  if (body.localAccess && typeof body.localAccess === 'object') {
4462
4521
  const localAccessBody = body.localAccess;
4463
4522
  if (localAccessBody.reset === true) {
@@ -4609,10 +4668,140 @@ router.get('/agent-plugins', (_req, res) => {
4609
4668
  hasResume: p.manifest.resume !== undefined,
4610
4669
  hasTitleNamer: p.manifest.titleNamer !== undefined,
4611
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),
4612
4679
  })),
4613
4680
  errors: errors.map((e) => ({ slug: e.slug, errors: e.errors, code: e.code, migration: e.migration })),
4614
4681
  });
4615
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/doctor', async (req, res) => {
4723
+ const result = await probePluginTitleNamer(req.params.slug);
4724
+ if (!result) {
4725
+ res.status(404).json({ error: `Plugin "${req.params.slug}" not found`, code: 'AGENT_PLUGIN_NOT_FOUND' });
4726
+ return;
4727
+ }
4728
+ res.json(result);
4729
+ });
4730
+ router.post('/agent-plugins/:slug/update', async (req, res) => {
4731
+ const slug = req.params.slug;
4732
+ const plugin = loadPlugins().plugins.find((entry) => entry.manifest.slug === slug);
4733
+ if (!plugin) {
4734
+ res.status(404).json({ error: `Plugin "${slug}" not found` });
4735
+ return;
4736
+ }
4737
+ if (!plugin.source?.source || plugin.source.type === 'manifest') {
4738
+ res.status(400).json({ error: 'This plugin has no update source', code: 'AGENT_PLUGIN_UPDATE_UNSUPPORTED' });
4739
+ return;
4740
+ }
4741
+ let prepared = null;
4742
+ let oldHooksRemoved = false;
4743
+ let committed = false;
4744
+ try {
4745
+ // Download and validate the complete replacement before touching the live
4746
+ // package or its currently installed hooks.
4747
+ prepared = await preparePluginPackage(plugin.source.source, slug);
4748
+ const hooksChanged = JSON.stringify(plugin.manifest.hooks ?? null) !== JSON.stringify(prepared.manifest.hooks ?? null);
4749
+ const titleCapabilityChanged = JSON.stringify(plugin.manifest.titleNamer ?? null) !== JSON.stringify(prepared.manifest.titleNamer ?? null);
4750
+ const hookState = (await listAllHookAgents()).find((entry) => entry.slug === slug)?.state;
4751
+ const restoreHooks = hookState === 'installed' || hookState === 'outdated';
4752
+ if (restoreHooks && plugin.manifest.hooks) {
4753
+ await uninstallHooksForSlug(slug);
4754
+ oldHooksRemoved = true;
4755
+ }
4756
+ const dir = await commitPreparedPlugin(prepared, true);
4757
+ prepared = null;
4758
+ committed = true;
4759
+ reloadAgentPlugins();
4760
+ let hookWarning = null;
4761
+ if (restoreHooks && hooksChanged) {
4762
+ hookWarning = 'Hook declarations changed during update. Review them, then install hooks again.';
4763
+ }
4764
+ else if (restoreHooks) {
4765
+ try {
4766
+ await installHooksForSlug(slug);
4767
+ }
4768
+ catch (error) {
4769
+ hookWarning = error.message;
4770
+ }
4771
+ }
4772
+ let titleWarning = null;
4773
+ if (titleCapabilityChanged) {
4774
+ setAutoRenameAgentsSetting(getAutoRenameAgentsSetting().filter((entry) => entry !== slug));
4775
+ if (getAutoRenameNamerSetting() === slug)
4776
+ setAutoRenameNamerSetting('auto');
4777
+ const models = getAutoRenameModelsSetting();
4778
+ delete models[slug];
4779
+ setAutoRenameModelsSetting(models);
4780
+ invalidateTitleNamerCatalog();
4781
+ titleWarning = 'Title command declarations changed during update. Automatic titles for this plugin were disabled; review and enable them again.';
4782
+ }
4783
+ res.json({
4784
+ slug,
4785
+ dir,
4786
+ state: 'updated',
4787
+ source: loadPlugins().plugins.find((entry) => entry.manifest.slug === slug)?.source,
4788
+ hookWarning,
4789
+ titleWarning,
4790
+ });
4791
+ }
4792
+ catch (error) {
4793
+ if (prepared)
4794
+ await prepared.cleanup().catch(() => undefined);
4795
+ if (oldHooksRemoved && !committed)
4796
+ await installHooksForSlug(slug).catch(() => undefined);
4797
+ const detail = error;
4798
+ res.status(400).json({
4799
+ error: detail.message,
4800
+ code: detail.code ?? 'AGENT_PLUGIN_UPDATE_FAILED',
4801
+ migration: detail.migration,
4802
+ });
4803
+ }
4804
+ });
4616
4805
  router.post('/agent-plugins', async (req, res) => {
4617
4806
  const manifest = req.body;
4618
4807
  if (!manifest || typeof manifest !== 'object') {
@@ -4638,10 +4827,7 @@ router.post('/agent-plugins', async (req, res) => {
4638
4827
  return;
4639
4828
  }
4640
4829
  const dir = savePlugin(validation.manifest);
4641
- invalidateTitleNamerCatalog();
4642
- clearPluginAgents();
4643
- const loaded = loadPlugins();
4644
- registerPluginAgents(loaded.plugins);
4830
+ reloadAgentPlugins();
4645
4831
  res.json({ slug: validation.manifest.slug, dir, state: 'created' });
4646
4832
  }
4647
4833
  catch (error) {
@@ -5126,7 +5312,7 @@ router.post('/:sessionId/input', express.text({ type: '*/*' }), (req, res) => {
5126
5312
  * bracketed paste. Refused while an agent turn is in flight — pasting a
5127
5313
  * resume into a busy TUI would corrupt its input.
5128
5314
  */
5129
- router.post('/:sessionId/agent-resume', (req, res) => {
5315
+ router.post('/:sessionId/agent-resume', async (req, res) => {
5130
5316
  const { sessionId } = req.params;
5131
5317
  const session = terminalSessions.get(sessionId);
5132
5318
  if (!session) {
@@ -5136,14 +5322,30 @@ router.post('/:sessionId/agent-resume', (req, res) => {
5136
5322
  if (session.agent && live && (live.status === 'working' || live.status === 'waiting')) {
5137
5323
  return res.status(409).json({ error: 'agent is busy; wait for the turn to finish' });
5138
5324
  }
5139
- const command = resolveAgentResumeCommand(sessionId, session);
5140
- if (!command) {
5325
+ const target = resolveAgentResumeTarget(sessionId, session);
5326
+ if (!target) {
5141
5327
  return res.status(404).json({ error: 'no resumable agent session for this pane' });
5142
5328
  }
5329
+ const activeOwner = findActiveAgentResumeOwner(sessionId, target);
5330
+ if (activeOwner) {
5331
+ return res.status(409).json({
5332
+ error: 'this agent session is already open in another terminal',
5333
+ code: 'AGENT_SESSION_ACTIVE_ELSEWHERE',
5334
+ activeBackendSessionId: activeOwner,
5335
+ });
5336
+ }
5337
+ const activeWriterPid = await findExternalCodexWriter(target);
5338
+ if (activeWriterPid) {
5339
+ return res.status(409).json({
5340
+ error: 'this Codex thread already has an active writer',
5341
+ code: 'AGENT_SESSION_ACTIVE_ELSEWHERE',
5342
+ activeWriterPid,
5343
+ });
5344
+ }
5143
5345
  try {
5144
- session.ptyProcess.write(buildBracketedSubmitBytes(command));
5346
+ session.ptyProcess.write(buildBracketedSubmitBytes(target.command));
5145
5347
  session.lastActivity = Date.now();
5146
- res.json({ success: true, command });
5348
+ res.json({ success: true, command: target.command });
5147
5349
  }
5148
5350
  catch (error) {
5149
5351
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -5151,16 +5353,25 @@ router.post('/:sessionId/agent-resume', (req, res) => {
5151
5353
  }
5152
5354
  });
5153
5355
  /** Whether this pane has a resumable agent conversation (for the UI affordance). */
5154
- router.get('/:sessionId/agent-resume', (req, res) => {
5356
+ router.get('/:sessionId/agent-resume', async (req, res) => {
5155
5357
  const { sessionId } = req.params;
5156
5358
  const session = terminalSessions.get(sessionId);
5157
5359
  if (!session) {
5158
5360
  return res.status(404).json({ error: 'Terminal session not found' });
5159
5361
  }
5160
- const command = resolveAgentResumeCommand(sessionId, session);
5362
+ const target = resolveAgentResumeTarget(sessionId, session);
5161
5363
  const busy = !!(session.agent && session.agentSession
5162
5364
  && (session.agentSession.status === 'working' || session.agentSession.status === 'waiting'));
5163
- res.json({ available: command !== null, command, busy });
5365
+ const activeBackendSessionId = target ? findActiveAgentResumeOwner(sessionId, target) : null;
5366
+ const activeWriterPid = target && !activeBackendSessionId ? await findExternalCodexWriter(target) : null;
5367
+ res.json({
5368
+ available: target !== null && activeBackendSessionId === null && activeWriterPid === null,
5369
+ command: target?.command ?? null,
5370
+ busy,
5371
+ conflict: activeBackendSessionId || activeWriterPid ? 'active-elsewhere' : null,
5372
+ activeBackendSessionId,
5373
+ activeWriterPid,
5374
+ });
5164
5375
  });
5165
5376
  router.post('/:sessionId/resize', async (req, res) => {
5166
5377
  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)) {
@@ -79,6 +79,12 @@ function normalizeSettings(value) {
79
79
  && typeof entry[1] === 'string'
80
80
  && /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,119}$/.test(entry[1]))))
81
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,
82
88
  updatedAt: typeof raw.updatedAt === 'number' ? raw.updatedAt : Date.now(),
83
89
  };
84
90
  }
@@ -218,6 +224,14 @@ export function setAutoRenameNamerSetting(namer) {
218
224
  export function getAutoRenameModelsSetting() {
219
225
  return { ...loadSettings().autoRenameModels };
220
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
+ }
221
235
  export function setAutoRenameModelsSetting(models) {
222
236
  return updateSettings((settings) => {
223
237
  settings.autoRenameModels = Object.fromEntries(Object.entries(models)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termdock",
3
- "version": "1.4.71",
3
+ "version": "1.4.73",
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.71",
4
+ "version": "1.4.73",
5
5
  "runtimeProtocolVersion": 1,
6
6
  "minimumDesktopVersion": "1.4.46",
7
7
  "nodeMajor": 22,
8
8
  "dependencyHash": "sha256-HxC6p3b4kFesefw+vmNYJtPtdIKJaZR1jFNb7DRJqjU=",
9
- "serverBundleHash": "sha256-P5Y6KKDEEsunDL2W8DPi22l8+/L6rehHwJAG6NLLwTc=",
9
+ "serverBundleHash": "sha256-xzvKVLcvFdBQsk4jGML445ipTKOjQeaGWTmNMudp3p0=",
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-CsAzdiCU.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-CSZrnmp_.js";import{b as i}from"./mermaid.core-CsAzdiCU.js";import"./chunk-5VM5RSS4-CjbUr-ko.js";import"./chunk-XXDRQBXY-CcZZlCik.js";import"./chunk-VR4S4FIN-BB-7zz-9.js";import"./chunk-32BRIVSS-CrqR5rvl.js";import"./purify.es-CC4Brkr_.js";import"./index-D5PKdbhW.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-CSZrnmp_.js";import{b as i}from"./mermaid.core-CsAzdiCU.js";import"./chunk-5VM5RSS4-CjbUr-ko.js";import"./chunk-XXDRQBXY-CcZZlCik.js";import"./chunk-VR4S4FIN-BB-7zz-9.js";import"./chunk-32BRIVSS-CrqR5rvl.js";import"./purify.es-CC4Brkr_.js";import"./index-D5PKdbhW.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};