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.
Files changed (84) hide show
  1. package/.desktop-dist/main.js +254 -49
  2. package/README.md +6 -0
  3. package/dist/client/assets/{ModelPreview-Bk8GM7G-.js → ModelPreview-CG6vLwyw.js} +1 -1
  4. package/dist/client/assets/{abnfDiagram-VRR7QNED-BYiQXGYk.js → abnfDiagram-VRR7QNED-CrXEZLWX.js} +1 -1
  5. package/dist/client/assets/{arc-jlT_jmoZ.js → arc-DbKGR7Ex.js} +1 -1
  6. package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-D1qZ875Q.js → architectureDiagram-ZJ3FMSHR-PF5BoUbI.js} +1 -1
  7. package/dist/client/assets/{blockDiagram-677ZJIJ3-qkgn2Ysc.js → blockDiagram-677ZJIJ3-D1gRzVWB.js} +1 -1
  8. package/dist/client/assets/{c4Diagram-LMCZKHZV-JgcH49Pf.js → c4Diagram-LMCZKHZV-CwvOLync.js} +1 -1
  9. package/dist/client/assets/channel-JTEuKVy6.js +1 -0
  10. package/dist/client/assets/{chunk-2Q5K7J3B-E-3iflEy.js → chunk-2Q5K7J3B-BeRdP-xP.js} +1 -1
  11. package/dist/client/assets/{chunk-32BRIVSS-mM01y3kF.js → chunk-32BRIVSS-CrqR5rvl.js} +1 -1
  12. package/dist/client/assets/{chunk-5VM5RSS4-GO8Muo5l.js → chunk-5VM5RSS4-CjbUr-ko.js} +1 -1
  13. package/dist/client/assets/{chunk-EX3LRPZG-BUSDluH0.js → chunk-EX3LRPZG-BVEk3Wma.js} +1 -1
  14. package/dist/client/assets/{chunk-JWPE2WC7-obSL6EzJ.js → chunk-JWPE2WC7-WitJe8kR.js} +1 -1
  15. package/dist/client/assets/{chunk-MOJQB5TN-zEktbtAG.js → chunk-MOJQB5TN-BCewlLYk.js} +1 -1
  16. package/dist/client/assets/{chunk-RYQCIY6F-kH7H8XgQ.js → chunk-RYQCIY6F-CoP0AA4G.js} +1 -1
  17. package/dist/client/assets/{chunk-V7JOEXUC-BqDFpHKN.js → chunk-V7JOEXUC-CSZrnmp_.js} +1 -1
  18. package/dist/client/assets/{chunk-VR4S4FIN-BGZ7kCIG.js → chunk-VR4S4FIN-BB-7zz-9.js} +1 -1
  19. package/dist/client/assets/{chunk-XXDRQBXY-D-TatWXe.js → chunk-XXDRQBXY-CcZZlCik.js} +1 -1
  20. package/dist/client/assets/classDiagram-OUVF2IWQ-BiFJB_5s.js +1 -0
  21. package/dist/client/assets/classDiagram-v2-EOCWNBFH-BiFJB_5s.js +1 -0
  22. package/dist/client/assets/{cose-bilkent-JH36ORCC-CfWyrViz.js → cose-bilkent-JH36ORCC-DcmzUDwh.js} +1 -1
  23. package/dist/client/assets/{cynefin-VYW2F7L2-BFdZ3FcE.js → cynefin-VYW2F7L2-C1XlDVoJ.js} +1 -1
  24. package/dist/client/assets/{cynefinDiagram-TSTJHNR4-iAfCVoFU.js → cynefinDiagram-TSTJHNR4-zY6Aet2S.js} +1 -1
  25. package/dist/client/assets/{dagre-VKFMJZFB-CZhh0hrr.js → dagre-VKFMJZFB-DqGTi6zR.js} +1 -1
  26. package/dist/client/assets/{diagram-FQU43EPY-BqUK2OmV.js → diagram-FQU43EPY-DStcsU7S.js} +1 -1
  27. package/dist/client/assets/{diagram-G47NLZAW-vC0pSQYC.js → diagram-G47NLZAW-Cs6IMNip.js} +1 -1
  28. package/dist/client/assets/{diagram-NH7WQ7WH-Dqc5oxed.js → diagram-NH7WQ7WH-yA2bpWN7.js} +1 -1
  29. package/dist/client/assets/{diagram-OA4YK3LP-DIIXckFE.js → diagram-OA4YK3LP-Dzf9M0PI.js} +1 -1
  30. package/dist/client/assets/{diagram-WEI45ONY-DR9_cFeh.js → diagram-WEI45ONY-D75lpQKA.js} +1 -1
  31. package/dist/client/assets/{ebnfDiagram-CCIWWBDH-21OO0OXC.js → ebnfDiagram-CCIWWBDH-CkQvIpWy.js} +1 -1
  32. package/dist/client/assets/{erDiagram-Q63AITRT-YKnVG3az.js → erDiagram-Q63AITRT-Ca00Vjxz.js} +1 -1
  33. package/dist/client/assets/{flowDiagram-23GEKE2U-Cm7TGErF.js → flowDiagram-23GEKE2U-CGrnSTEp.js} +1 -1
  34. package/dist/client/assets/{ganttDiagram-NO4QXBWP-D_76Effz.js → ganttDiagram-NO4QXBWP-0SHfOIP4.js} +1 -1
  35. package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-CC1ysmVD.js → gitGraphDiagram-IHSO6WYX-BeigYWvs.js} +1 -1
  36. package/dist/client/assets/index-D5PKdbhW.js +451 -0
  37. package/dist/client/assets/index-DhfIiBLW.css +1 -0
  38. package/dist/client/assets/{infoDiagram-FWYZ7A6U-BbgpmhBz.js → infoDiagram-FWYZ7A6U-CiY1-xjN.js} +1 -1
  39. package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-C94bY_je.js → ishikawaDiagram-FXEZZL3T-CmtDjUm_.js} +1 -1
  40. package/dist/client/assets/{journeyDiagram-5HDEW3XC-BQ0iFRxs.js → journeyDiagram-5HDEW3XC-B7qOr9LY.js} +1 -1
  41. package/dist/client/assets/{kanban-definition-HUTT4EX6-BtAexXPo.js → kanban-definition-HUTT4EX6-DO2P2Um3.js} +1 -1
  42. package/dist/client/assets/{linear-C2HnIdOK.js → linear-B832hJt0.js} +1 -1
  43. package/dist/client/assets/{mermaid.core-BbG4sJBF.js → mermaid.core-CsAzdiCU.js} +4 -4
  44. package/dist/client/assets/{mindmap-definition-LN4V7U3C-B-uvViws.js → mindmap-definition-LN4V7U3C-HI__vXhQ.js} +1 -1
  45. package/dist/client/assets/{pegDiagram-2B236MQR-DHYcR6tE.js → pegDiagram-2B236MQR--rZIwjXq.js} +1 -1
  46. package/dist/client/assets/{pieDiagram-ENE6RG2P-DEE9cD6M.js → pieDiagram-ENE6RG2P-D9jPf5x9.js} +1 -1
  47. package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-DMQP5jG3.js → quadrantDiagram-ABIIQ3AL-DTCP5ji2.js} +1 -1
  48. package/dist/client/assets/{railroadDiagram-RFXS5EU6-B-4WDTwc.js → railroadDiagram-RFXS5EU6-D2Gspvrl.js} +1 -1
  49. package/dist/client/assets/{requirementDiagram-TGXJPOKE-C2xw_q-R.js → requirementDiagram-TGXJPOKE-DEJC02IW.js} +1 -1
  50. package/dist/client/assets/{sankeyDiagram-HTMAVEWB-wq0BPqhc.js → sankeyDiagram-HTMAVEWB-opOhKPap.js} +1 -1
  51. package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-BqpM0A9d.js → sequenceDiagram-DBY2YBRQ-_sz2CRAG.js} +1 -1
  52. package/dist/client/assets/{sizeCapture-X5ZJPWSS-Ba2xxyp_.js → sizeCapture-X5ZJPWSS-D0Q2QkYF.js} +1 -1
  53. package/dist/client/assets/{stateDiagram-2N3HPSRC-agPx5qAN.js → stateDiagram-2N3HPSRC-VgeSQTtW.js} +1 -1
  54. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-BVh2FRO-.js +1 -0
  55. package/dist/client/assets/{swimlanes-5IMT3BWC-Cy6Lzj_L.js → swimlanes-5IMT3BWC-ClvBvSl0.js} +2 -2
  56. package/dist/client/assets/swimlanesDiagram-G3AALYLV-C17V-kgm.js +8 -0
  57. package/dist/client/assets/{timeline-definition-FHXFAJF6-LBrVfbdp.js → timeline-definition-FHXFAJF6-BXNsrECV.js} +1 -1
  58. package/dist/client/assets/{vennDiagram-L72KCM5P-BiYIV7mX.js → vennDiagram-L72KCM5P-BgwiqZd-.js} +1 -1
  59. package/dist/client/assets/{wardleyDiagram-EHGQE667-Q1QU9gG3.js → wardleyDiagram-EHGQE667-DiqN5ERx.js} +1 -1
  60. package/dist/client/assets/{xychartDiagram-FW5EYKEG-CzYpxk4l.js → xychartDiagram-FW5EYKEG-DQjFVqC6.js} +1 -1
  61. package/dist/client/index.html +2 -2
  62. package/dist/client/sw.js +1 -1
  63. package/dist/server/agent/autoTitle.js +95 -0
  64. package/dist/server/agent/installers.js +106 -22
  65. package/dist/server/agent/plugins.js +135 -4
  66. package/dist/server/agent/registry.js +2 -1
  67. package/dist/server/agent/session.js +27 -2
  68. package/dist/server/agent/titleNamerCatalog.js +301 -0
  69. package/dist/server/agentHook.js +49 -1
  70. package/dist/server/cli.js +238 -30
  71. package/dist/server/routes/filesystem.js +11 -8
  72. package/dist/server/routes/terminal.js +382 -14
  73. package/dist/server/utils/autoUpdate.js +175 -0
  74. package/dist/server/utils/npmUpdate.js +2 -1
  75. package/dist/server/utils/settings.js +43 -0
  76. package/package.json +1 -1
  77. package/runtime-manifest.json +2 -2
  78. package/dist/client/assets/channel-C2i7H7bH.js +0 -1
  79. package/dist/client/assets/classDiagram-OUVF2IWQ-BlGG6XFj.js +0 -1
  80. package/dist/client/assets/classDiagram-v2-EOCWNBFH-BlGG6XFj.js +0 -1
  81. package/dist/client/assets/index-ChhyERKX.css +0 -1
  82. package/dist/client/assets/index-r-PkH7SB.js +0 -442
  83. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-beoQXpWJ.js +0 -1
  84. package/dist/client/assets/swimlanesDiagram-G3AALYLV-BxL1xNhC.js +0 -8
@@ -0,0 +1,301 @@
1
+ import { execFile, spawn } from 'child_process';
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
5
+ import { promisify } from 'util';
6
+ import { loadPlugins } from './plugins.js';
7
+ const execFileAsync = promisify(execFile);
8
+ const CACHE_FRESH_MS = 24 * 60 * 60_000;
9
+ const PROBE_TIMEOUT_MS = 10_000;
10
+ const CACHE_FILE = path.join(os.homedir(), '.termdock', 'title-namer-catalog.json');
11
+ let cached = null;
12
+ let refreshPromise = null;
13
+ export function invalidateTitleNamerCatalog() {
14
+ cached = null;
15
+ }
16
+ function normalizeCachedCatalog(input) {
17
+ if (!input || typeof input !== 'object')
18
+ return null;
19
+ const doc = input;
20
+ if (doc.version !== 1 || typeof doc.updatedAt !== 'number' || !Array.isArray(doc.value))
21
+ return null;
22
+ const value = doc.value.flatMap((entry) => {
23
+ if (!entry || typeof entry !== 'object')
24
+ return [];
25
+ const candidate = entry;
26
+ if (typeof candidate.slug !== 'string' || !/^[a-z][a-z0-9-]{0,39}$/.test(candidate.slug))
27
+ return [];
28
+ if (!Array.isArray(candidate.models))
29
+ return [];
30
+ const models = candidate.models.filter((model) => Boolean(model
31
+ && typeof model.id === 'string'
32
+ && typeof model.displayName === 'string'
33
+ && typeof model.description === 'string'
34
+ && typeof model.isDefault === 'boolean'));
35
+ return [{
36
+ slug: candidate.slug,
37
+ displayName: typeof candidate.displayName === 'string' ? candidate.displayName : candidate.slug,
38
+ available: candidate.available === true,
39
+ models,
40
+ recommendedModel: typeof candidate.recommendedModel === 'string'
41
+ ? candidate.recommendedModel
42
+ : recommendTitleModel(models),
43
+ }];
44
+ });
45
+ return value.length > 0 ? { version: 1, updatedAt: doc.updatedAt, value } : null;
46
+ }
47
+ async function loadPersistentCache() {
48
+ try {
49
+ return normalizeCachedCatalog(JSON.parse(await fs.promises.readFile(CACHE_FILE, 'utf8')));
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ async function savePersistentCache(doc) {
56
+ const directory = path.dirname(CACHE_FILE);
57
+ const temporary = `${CACHE_FILE}.${process.pid}.tmp`;
58
+ await fs.promises.mkdir(directory, { recursive: true });
59
+ await fs.promises.writeFile(temporary, JSON.stringify(doc, null, 2), 'utf8');
60
+ await fs.promises.rename(temporary, CACHE_FILE);
61
+ }
62
+ export function recommendTitleModel(models) {
63
+ return models.find((model) => /affordable|cost[- ]efficient|cost[- ]sensitive/i.test(model.description))?.id
64
+ ?? models.find((model) => model.isDefault)?.id
65
+ ?? null;
66
+ }
67
+ async function listCodexModels() {
68
+ return new Promise((resolve, reject) => {
69
+ const child = spawn('codex', ['app-server', '--stdio'], {
70
+ stdio: ['pipe', 'pipe', 'ignore'],
71
+ env: { ...process.env, NO_COLOR: '1', TERM: 'dumb' },
72
+ });
73
+ let buffer = '';
74
+ let settled = false;
75
+ const finish = (models, error) => {
76
+ if (settled)
77
+ return;
78
+ settled = true;
79
+ clearTimeout(timer);
80
+ child.kill();
81
+ if (error)
82
+ reject(error);
83
+ else
84
+ resolve(models ?? []);
85
+ };
86
+ const timer = setTimeout(() => finish(undefined, new Error('Codex model discovery timed out')), PROBE_TIMEOUT_MS);
87
+ timer.unref?.();
88
+ child.on('error', (error) => finish(undefined, error));
89
+ child.stdout.on('data', (chunk) => {
90
+ buffer += chunk.toString();
91
+ for (;;) {
92
+ const newline = buffer.indexOf('\n');
93
+ if (newline < 0)
94
+ break;
95
+ const line = buffer.slice(0, newline).trim();
96
+ buffer = buffer.slice(newline + 1);
97
+ if (!line)
98
+ continue;
99
+ let message;
100
+ try {
101
+ message = JSON.parse(line);
102
+ }
103
+ catch {
104
+ continue;
105
+ }
106
+ if (message.id === 1) {
107
+ child.stdin.write(`${JSON.stringify({ method: 'initialized', params: {} })}\n`);
108
+ child.stdin.write(`${JSON.stringify({ id: 2, method: 'model/list', params: { limit: 100 } })}\n`);
109
+ }
110
+ else if (message.id === 2) {
111
+ const data = message.result?.data;
112
+ const models = Array.isArray(data) ? data.flatMap((entry) => {
113
+ if (!entry || typeof entry !== 'object')
114
+ return [];
115
+ const model = entry;
116
+ if (typeof model.model !== 'string' || typeof model.displayName !== 'string')
117
+ return [];
118
+ return [{
119
+ id: model.model,
120
+ displayName: model.displayName,
121
+ description: typeof model.description === 'string' ? model.description : '',
122
+ isDefault: model.isDefault === true,
123
+ }];
124
+ }) : [];
125
+ finish(models);
126
+ }
127
+ }
128
+ });
129
+ child.stdin.write(`${JSON.stringify({
130
+ id: 1,
131
+ method: 'initialize',
132
+ params: { clientInfo: { name: 'termdock', version: '1' } },
133
+ })}\n`);
134
+ });
135
+ }
136
+ export function parseClaudeSupportedModels(output) {
137
+ const match = output.match(/supported (?:API )?model names are ([^\n]+?)(?:,?\s+but you passed|$)/i);
138
+ if (!match)
139
+ return [];
140
+ return match[1]
141
+ .split(/,|\band\b/i)
142
+ .map((model) => model.trim().replace(/^["'`]+|["'`]+$/g, ''))
143
+ .filter((model) => /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,119}$/.test(model))
144
+ .map((model) => ({ id: model, displayName: model, description: '', isDefault: false }));
145
+ }
146
+ async function listClaudeModels() {
147
+ try {
148
+ await execFileAsync('claude', ['--model', '__termdock_model_probe__', '--no-session-persistence', '-p', 'x'], {
149
+ timeout: PROBE_TIMEOUT_MS,
150
+ maxBuffer: 128 * 1024,
151
+ env: { ...process.env, NO_COLOR: '1', TERM: 'dumb' },
152
+ });
153
+ return [];
154
+ }
155
+ catch (error) {
156
+ const candidate = error;
157
+ if (candidate.code === 'ENOENT')
158
+ throw error;
159
+ return parseClaudeSupportedModels(`${candidate.stdout ?? ''}\n${candidate.stderr ?? ''}`);
160
+ }
161
+ }
162
+ function normalizeDiscoveredModels(input) {
163
+ if (!Array.isArray(input))
164
+ return [];
165
+ return input.flatMap((entry) => {
166
+ if (typeof entry === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,119}$/.test(entry)) {
167
+ return [{ id: entry, displayName: entry, description: '', isDefault: false }];
168
+ }
169
+ if (!entry || typeof entry !== 'object')
170
+ return [];
171
+ const model = entry;
172
+ if (typeof model.id !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,119}$/.test(model.id))
173
+ return [];
174
+ return [{
175
+ id: model.id,
176
+ displayName: typeof model.displayName === 'string' ? model.displayName : model.id,
177
+ description: typeof model.description === 'string' ? model.description : '',
178
+ isDefault: model.isDefault === true,
179
+ }];
180
+ });
181
+ }
182
+ async function listPluginModels(config) {
183
+ if (!config.models)
184
+ return [];
185
+ const { stdout } = await execFileAsync(config.models.command, config.models.args ?? [], {
186
+ timeout: PROBE_TIMEOUT_MS,
187
+ maxBuffer: 256 * 1024,
188
+ env: { ...process.env, NO_COLOR: '1', TERM: 'dumb' },
189
+ });
190
+ return normalizeDiscoveredModels(JSON.parse(stdout));
191
+ }
192
+ function pluginTitleNamers() {
193
+ return loadPlugins().plugins.flatMap((plugin) => plugin.manifest.titleNamer
194
+ ? [{
195
+ slug: plugin.manifest.slug,
196
+ displayName: plugin.manifest.displayName,
197
+ config: plugin.manifest.titleNamer,
198
+ }]
199
+ : []);
200
+ }
201
+ export async function runTitleNamer(slug, prompt, model) {
202
+ let command;
203
+ let args;
204
+ if (slug === 'codex') {
205
+ command = 'codex';
206
+ args = ['exec', '--ephemeral', '--skip-git-repo-check', '--color', 'never', ...(model ? ['--model', model] : []), prompt];
207
+ }
208
+ else if (slug === 'claude') {
209
+ command = 'claude';
210
+ args = [...(model ? ['--model', model] : []), '--no-session-persistence', '-p', prompt];
211
+ }
212
+ else {
213
+ const provider = pluginTitleNamers().find((entry) => entry.slug === slug);
214
+ if (!provider)
215
+ return null;
216
+ command = provider.config.command;
217
+ args = provider.config.args.flatMap((arg) => {
218
+ if (arg.includes('{model}') && !model)
219
+ return [];
220
+ return [arg.replaceAll('{prompt}', prompt).replaceAll('{model}', model ?? '')];
221
+ });
222
+ }
223
+ try {
224
+ const { stdout } = await execFileAsync(command, args, {
225
+ timeout: 45_000,
226
+ maxBuffer: 256 * 1024,
227
+ env: { ...process.env, NO_COLOR: '1', TERM: 'dumb' },
228
+ });
229
+ return stdout;
230
+ }
231
+ catch {
232
+ return null;
233
+ }
234
+ }
235
+ async function refreshTitleNamerCatalog() {
236
+ if (refreshPromise)
237
+ return refreshPromise;
238
+ refreshPromise = (async () => {
239
+ const plugins = pluginTitleNamers();
240
+ const [codex, claude, ...pluginResults] = await Promise.allSettled([
241
+ listCodexModels(),
242
+ listClaudeModels(),
243
+ ...plugins.map((provider) => listPluginModels(provider.config)),
244
+ ]);
245
+ const previousCodex = cached?.value.find((namer) => namer.slug === 'codex');
246
+ const previousClaude = cached?.value.find((namer) => namer.slug === 'claude');
247
+ const codexModels = codex.status === 'fulfilled' ? codex.value : previousCodex?.models ?? [];
248
+ const claudeModels = claude.status === 'fulfilled' ? claude.value : previousClaude?.models ?? [];
249
+ const value = [
250
+ {
251
+ slug: 'codex',
252
+ displayName: 'Codex',
253
+ available: codex.status === 'fulfilled' || previousCodex?.available === true,
254
+ models: codexModels,
255
+ recommendedModel: recommendTitleModel(codexModels),
256
+ },
257
+ {
258
+ slug: 'claude',
259
+ displayName: 'Claude Code',
260
+ available: claude.status === 'fulfilled' || previousClaude?.available === true,
261
+ models: claudeModels,
262
+ recommendedModel: recommendTitleModel(claudeModels),
263
+ },
264
+ ...plugins.map((provider, index) => {
265
+ const result = pluginResults[index];
266
+ const previous = cached?.value.find((namer) => namer.slug === provider.slug);
267
+ const models = result?.status === 'fulfilled' ? result.value : previous?.models ?? [];
268
+ return {
269
+ slug: provider.slug,
270
+ displayName: provider.displayName,
271
+ available: provider.config.models ? result?.status === 'fulfilled' || previous?.available === true : true,
272
+ models,
273
+ recommendedModel: recommendTitleModel(models),
274
+ };
275
+ }),
276
+ ];
277
+ cached = { version: 1, updatedAt: Date.now(), value };
278
+ await savePersistentCache(cached).catch(() => undefined);
279
+ return value;
280
+ })().finally(() => {
281
+ refreshPromise = null;
282
+ });
283
+ return refreshPromise;
284
+ }
285
+ export async function getTitleNamerCatalog(force = false) {
286
+ if (force)
287
+ return refreshTitleNamerCatalog();
288
+ if (!cached)
289
+ cached = await loadPersistentCache();
290
+ if (!cached)
291
+ return refreshTitleNamerCatalog();
292
+ const expectedSlugs = new Set(['codex', 'claude', ...pluginTitleNamers().map((provider) => provider.slug)]);
293
+ if (cached.value.length !== expectedSlugs.size || cached.value.some((entry) => !expectedSlugs.has(entry.slug))) {
294
+ return refreshTitleNamerCatalog();
295
+ }
296
+ if (Date.now() - cached.updatedAt > CACHE_FRESH_MS) {
297
+ // Stale-while-revalidate: callers get the last known catalog immediately.
298
+ void refreshTitleNamerCatalog();
299
+ }
300
+ return cached.value;
301
+ }
@@ -103,6 +103,47 @@ function ancestorTtyDevice() {
103
103
  }
104
104
  return null;
105
105
  }
106
+ /**
107
+ * tmux passthrough is version/configuration sensitive and, more importantly,
108
+ * only works while tmux considers the escape sequence pane output. Termdock
109
+ * itself is an attached tmux client, so write the sentinel to that client's
110
+ * tty directly when possible. This also works for detached hook subprocesses
111
+ * whose own `/dev/tty` is unavailable.
112
+ */
113
+ function tmuxClientTtyDevices() {
114
+ const pane = process.env.TMUX_PANE;
115
+ if (!process.env.TMUX || !pane)
116
+ return [];
117
+ try {
118
+ const output = execFileSync('tmux', ['list-clients', '-t', pane, '-F', '#{client_tty}\t#{client_pid}'], {
119
+ encoding: 'utf8',
120
+ stdio: ['ignore', 'pipe', 'ignore'],
121
+ timeout: 2000,
122
+ });
123
+ return [...new Set(output.split(/\r?\n/).flatMap((line) => {
124
+ const [device, pid] = line.trim().split('\t');
125
+ if (!/^\/dev\/(?:pts\/\d+|tty[^/]*)$/.test(device ?? ''))
126
+ return [];
127
+ // On Linux the environment gives us an exact ownership marker. An
128
+ // ordinary `tmux attach` must not steal a Termdock sentinel merely
129
+ // because tmux listed that client first.
130
+ const environ = pid && /^\d+$/.test(pid) ? `/proc/${pid}/environ` : null;
131
+ if (environ && fs.existsSync(environ)) {
132
+ try {
133
+ if (!fs.readFileSync(environ).toString().split('\0').includes('TERMDOCK=1'))
134
+ return [];
135
+ }
136
+ catch {
137
+ return [];
138
+ }
139
+ }
140
+ return [device];
141
+ }))];
142
+ }
143
+ catch {
144
+ return [];
145
+ }
146
+ }
106
147
  /**
107
148
  * Write the sequence to the pane's PTY so the server sniffs it as pane output.
108
149
  * Two routes: /dev/tty (hook attached to its tty), or an ancestor's tty
@@ -116,6 +157,12 @@ function ancestorTtyDevice() {
116
157
  * the termdock server — as the plain inner OSC.
117
158
  */
118
159
  function writeToControllingTty(sequence, inTmuxPane) {
160
+ if (inTmuxPane) {
161
+ const clientTtys = tmuxClientTtyDevices();
162
+ if (clientTtys.length > 0) {
163
+ return clientTtys.some((device) => writeDev(device, sequence));
164
+ }
165
+ }
119
166
  // Only genuine tmux panes (TMUX_PANE is set by tmux itself) get the DCS
120
167
  // passthrough wrap; a bare inherited TMUX just means an ancestor ran tmux.
121
168
  const wire = inTmuxPane
@@ -145,6 +192,7 @@ function main() {
145
192
  args.shift();
146
193
  let agent = args[0] ?? '';
147
194
  const eventArg = args[1] ?? '';
195
+ const statusArg = args[2] ?? '';
148
196
  if (!agent || !eventArg)
149
197
  return;
150
198
  if (process.env[GROK_HOOK_ENV])
@@ -164,7 +212,7 @@ function main() {
164
212
  const event = effectiveEvent(agent, eventArg, input);
165
213
  if (!event)
166
214
  return;
167
- writeToControllingTty(buildHookSequence(agent, event, input), inTmuxPane);
215
+ writeToControllingTty(buildHookSequence(agent, event, input, statusArg || undefined), inTmuxPane);
168
216
  }
169
217
  catch {
170
218
  // Never break the agent's flow.