nterminal 1.3.4 → 1.3.5
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/bin/nterminal.js +8 -3
- package/dist/client/assets/MarkdownPreview-BIrwUAaa.js +1 -0
- package/dist/client/assets/{TranscriptMarkdownBody-H8ZsISc6.js → TranscriptMarkdownBody-CI0ztvMX.js} +1 -1
- package/dist/client/assets/{index-DxfBq-4P.js → index-EN8ngMgv.js} +1 -1
- package/dist/client/assets/index-JQJ1Eet1.js +58 -0
- package/dist/client/assets/index-xkL4h3qt.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/scripts/hooks.js +83 -0
- package/dist/scripts/hooks.js.map +1 -0
- package/dist/server/accounts/accountService.d.ts +23 -1
- package/dist/server/accounts/accountService.js +235 -103
- package/dist/server/accounts/accountService.js.map +1 -1
- package/dist/server/accounts/providerRequests.d.ts +12 -0
- package/dist/server/accounts/providerRequests.js +159 -0
- package/dist/server/accounts/providerRequests.js.map +1 -0
- package/dist/server/agent/agentRoutes.js +11 -0
- package/dist/server/agent/agentRoutes.js.map +1 -1
- package/dist/server/hooks/helperJournal.d.ts +16 -0
- package/dist/server/hooks/helperJournal.js +74 -0
- package/dist/server/hooks/helperJournal.js.map +1 -0
- package/dist/server/hooks/hookBinding.d.ts +39 -0
- package/dist/server/hooks/hookBinding.js +91 -0
- package/dist/server/hooks/hookBinding.js.map +1 -0
- package/dist/server/hooks/hookFiles.d.ts +24 -0
- package/dist/server/hooks/hookFiles.js +160 -0
- package/dist/server/hooks/hookFiles.js.map +1 -0
- package/dist/server/hooks/hookInstallation.d.ts +79 -0
- package/dist/server/hooks/hookInstallation.js +978 -0
- package/dist/server/hooks/hookInstallation.js.map +1 -0
- package/dist/server/hooks/hookProcess.d.ts +5 -0
- package/dist/server/hooks/hookProcess.js +85 -0
- package/dist/server/hooks/hookProcess.js.map +1 -0
- package/dist/server/hooks/hookReceiver.d.ts +66 -0
- package/dist/server/hooks/hookReceiver.js +549 -0
- package/dist/server/hooks/hookReceiver.js.map +1 -0
- package/dist/server/hooks/lifecycle.d.ts +40 -0
- package/dist/server/hooks/lifecycle.js +200 -0
- package/dist/server/hooks/lifecycle.js.map +1 -0
- package/dist/server/hooks/publicationLock.d.ts +3 -0
- package/dist/server/hooks/publicationLock.js +137 -0
- package/dist/server/hooks/publicationLock.js.map +1 -0
- package/dist/server/hooks/verifiedLock.d.ts +24 -0
- package/dist/server/hooks/verifiedLock.js +207 -0
- package/dist/server/hooks/verifiedLock.js.map +1 -0
- package/dist/server/http.d.ts +4 -0
- package/dist/server/http.js +6 -0
- package/dist/server/http.js.map +1 -1
- package/dist/server/index.js +69 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/notifications/agentCompletionMonitor.d.ts +16 -1
- package/dist/server/notifications/agentCompletionMonitor.js +261 -47
- package/dist/server/notifications/agentCompletionMonitor.js.map +1 -1
- package/dist/server/notifications/completionNotificationRouter.d.ts +2 -0
- package/dist/server/notifications/completionNotificationRouter.js +9 -0
- package/dist/server/notifications/completionNotificationRouter.js.map +1 -1
- package/dist/server/routes/accountRoutes.js +11 -0
- package/dist/server/routes/accountRoutes.js.map +1 -1
- package/dist/server/routes/activityRoutes.d.ts +13 -0
- package/dist/server/routes/activityRoutes.js +165 -0
- package/dist/server/routes/activityRoutes.js.map +1 -0
- package/dist/server/routes/agentManagementRoutes.js +8 -1
- package/dist/server/routes/agentManagementRoutes.js.map +1 -1
- package/dist/server/routes/hookRoutes.d.ts +16 -0
- package/dist/server/routes/hookRoutes.js +77 -0
- package/dist/server/routes/hookRoutes.js.map +1 -0
- package/dist/server/routes/terminalRoutes.js +4 -0
- package/dist/server/routes/terminalRoutes.js.map +1 -1
- package/dist/server/routes/transcriptQuery.d.ts +2 -1
- package/dist/server/routes/transcriptQuery.js +3 -1
- package/dist/server/routes/transcriptQuery.js.map +1 -1
- package/dist/server/terminal/PtyAdapter.d.ts +18 -0
- package/dist/server/terminal/PtyAdapter.js.map +1 -1
- package/dist/server/terminal/TerminalManager.d.ts +61 -0
- package/dist/server/terminal/TerminalManager.js +665 -12
- package/dist/server/terminal/TerminalManager.js.map +1 -1
- package/dist/server/terminal/TmuxPtyAdapter.d.ts +14 -2
- package/dist/server/terminal/TmuxPtyAdapter.js +99 -15
- package/dist/server/terminal/TmuxPtyAdapter.js.map +1 -1
- package/dist/server/terminal/activityChannel.d.ts +57 -0
- package/dist/server/terminal/activityChannel.js +166 -0
- package/dist/server/terminal/activityChannel.js.map +1 -0
- package/dist/server/terminal/agentTranscriptStatus.d.ts +12 -0
- package/dist/server/terminal/agentTranscriptStatus.js +78 -7
- package/dist/server/terminal/agentTranscriptStatus.js.map +1 -1
- package/dist/server/terminal/codexTranscriptSource.d.ts +2 -0
- package/dist/server/terminal/codexTranscriptSource.js +41 -37
- package/dist/server/terminal/codexTranscriptSource.js.map +1 -1
- package/dist/server/terminal/subagentSources.js +2 -1
- package/dist/server/terminal/subagentSources.js.map +1 -1
- package/dist/server/terminal/transcriptHeader.d.ts +85 -0
- package/dist/server/terminal/transcriptHeader.js +376 -0
- package/dist/server/terminal/transcriptHeader.js.map +1 -0
- package/dist/server/terminal/transcriptHistory.d.ts +16 -2
- package/dist/server/terminal/transcriptHistory.js +120 -21
- package/dist/server/terminal/transcriptHistory.js.map +1 -1
- package/dist/server/terminal/transcriptStore.d.ts +6 -0
- package/dist/server/terminal/transcriptStore.js +222 -26
- package/dist/server/terminal/transcriptStore.js.map +1 -1
- package/dist/shared/activityProbe.d.ts +10 -0
- package/dist/shared/activityProbe.js +24 -0
- package/dist/shared/activityProbe.js.map +1 -0
- package/dist/shared/protocol.d.ts +45 -0
- package/dist/shared/protocol.js.map +1 -1
- package/dist/shared/types.d.ts +9 -1
- package/docs/features.md +10 -0
- package/docs/provider-hooks.md +287 -0
- package/docs/terminal-history.md +10 -0
- package/package.json +2 -1
- package/scripts/nterminal-hook.cjs +171 -0
- package/scripts/nterminalctl +26 -2
- package/dist/client/assets/MarkdownPreview-DjDO4lEl.js +0 -1
- package/dist/client/assets/index-CIYazAGv.css +0 -1
- package/dist/client/assets/index-DwvLN_WJ.js +0 -56
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
import * as nodePty from 'node-pty';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { normalizeTerminalTitle, PtySessionUnavailableError } from './PtyAdapter.js';
|
|
10
|
-
import {
|
|
10
|
+
import { lookupCodexTranscriptFromDaemon, lookupCodexTranscriptFromProcessLogs, lookupCodexTranscriptFromOpenFds, resolveCodexTranscriptCandidates, processBinding, TRANSCRIPT_CANDIDATE_LIMIT } from './codexTranscriptSource.js';
|
|
11
11
|
import { isSessionId } from './sessionId.js';
|
|
12
12
|
import { processStartTicksToSecondsAsync, readProcessCwdAsync, readProcessEnvValuesAsync, readProcessStartTicksAsync } from './procfs.js';
|
|
13
13
|
import { TtlCache } from './ttlCache.js';
|
|
@@ -26,6 +26,8 @@ const TRANSCRIPT_FILE_NEGATIVE_CACHE_TTL_MS = 20_000;
|
|
|
26
26
|
// letting a history-paging burst (several page requests within seconds)
|
|
27
27
|
// reuse one resolution instead of re-forking tmux/ps per page.
|
|
28
28
|
const TRANSCRIPT_RESOLUTION_CACHE_TTL_MS = 3_000;
|
|
29
|
+
// Diagnostics outlive a few status polls, not the pane's next resolution.
|
|
30
|
+
const TRANSCRIPT_AMBIGUITY_TTL_MS = 90_000;
|
|
29
31
|
const TRANSCRIPT_CANDIDATE_CACHE_TTL_MS = 500;
|
|
30
32
|
const PROCESS_TREE_CACHE_TTL_MS = 1_000;
|
|
31
33
|
const CLAUDE_TRANSCRIPT_START_SKEW_MS = 10_000;
|
|
@@ -75,6 +77,9 @@ export class TmuxPtyAdapter {
|
|
|
75
77
|
commandRunner;
|
|
76
78
|
ptySpawner;
|
|
77
79
|
transcriptResolutionCache = new TtlCache();
|
|
80
|
+
transcriptResolutionGeneration = new Map();
|
|
81
|
+
// Last discovery outcome per pane, for status reporting only (bounded by panes).
|
|
82
|
+
transcriptAmbiguity = new Map();
|
|
78
83
|
transcriptResolutionInflight = new KeyedSingleFlight();
|
|
79
84
|
transcriptCandidateInflight = new KeyedSingleFlight();
|
|
80
85
|
processTreeCache = new TtlCache();
|
|
@@ -397,6 +402,8 @@ export class TmuxPtyAdapter {
|
|
|
397
402
|
this.historyOutputGenerationBySession.delete(sessionId);
|
|
398
403
|
this.historyOutputUnreliableSessions.delete(sessionId);
|
|
399
404
|
this.transcriptResolutionCache.delete(sessionId);
|
|
405
|
+
this.transcriptResolutionGeneration.delete(sessionId);
|
|
406
|
+
this.transcriptAmbiguity.delete(sessionId);
|
|
400
407
|
}
|
|
401
408
|
isInflightFinalCreation(sessionId, marker) {
|
|
402
409
|
const inflightMarker = this.inflightFinalCreationMarkers.get(sessionId);
|
|
@@ -1039,16 +1046,50 @@ export class TmuxPtyAdapter {
|
|
|
1039
1046
|
}
|
|
1040
1047
|
}
|
|
1041
1048
|
async resolveTranscriptSource(sessionId) {
|
|
1049
|
+
return (await this.resolveTranscriptSourceEvidence(sessionId))?.source ?? null;
|
|
1050
|
+
}
|
|
1051
|
+
async resolveTranscriptSourceEvidence(sessionId, options = {}) {
|
|
1052
|
+
const resolved = options.fresh ? await this.resolveTranscriptSourceUncached(sessionId, true)
|
|
1053
|
+
: await this.resolveTranscriptSourceCached(sessionId);
|
|
1054
|
+
if (!resolved)
|
|
1055
|
+
return null;
|
|
1056
|
+
const { evidence = 'inferred', client, ...source } = resolved;
|
|
1057
|
+
return { source, evidence, ...(client ? { client } : {}) };
|
|
1058
|
+
}
|
|
1059
|
+
getTranscriptAmbiguity(sessionId) {
|
|
1060
|
+
const entry = this.transcriptAmbiguity.get(sessionId);
|
|
1061
|
+
if (!entry)
|
|
1062
|
+
return null;
|
|
1063
|
+
if (Date.now() - entry.at > TRANSCRIPT_AMBIGUITY_TTL_MS) {
|
|
1064
|
+
this.transcriptAmbiguity.delete(sessionId);
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
return entry.reason;
|
|
1068
|
+
}
|
|
1069
|
+
noteTranscriptAmbiguity(sessionId, ambiguous) {
|
|
1070
|
+
if (ambiguous)
|
|
1071
|
+
this.transcriptAmbiguity.set(sessionId, { reason: 'codex_shared_daemon_same_cwd', at: Date.now() });
|
|
1072
|
+
else
|
|
1073
|
+
this.transcriptAmbiguity.delete(sessionId);
|
|
1074
|
+
}
|
|
1075
|
+
async resolveTranscriptSourceCached(sessionId) {
|
|
1042
1076
|
const cached = this.transcriptResolutionCache.get(sessionId);
|
|
1043
1077
|
if (cached) {
|
|
1044
1078
|
return cached.value;
|
|
1045
1079
|
}
|
|
1046
|
-
|
|
1080
|
+
const generation = this.transcriptResolutionGeneration.get(sessionId) ?? 0;
|
|
1081
|
+
return this.transcriptResolutionInflight.run(`${sessionId}:${generation}`, async () => {
|
|
1047
1082
|
const source = await this.resolveTranscriptSourceUncached(sessionId);
|
|
1048
|
-
this.
|
|
1083
|
+
if ((this.transcriptResolutionGeneration.get(sessionId) ?? 0) === generation) {
|
|
1084
|
+
this.transcriptResolutionCache.set(sessionId, source, TRANSCRIPT_RESOLUTION_CACHE_TTL_MS);
|
|
1085
|
+
}
|
|
1049
1086
|
return source;
|
|
1050
1087
|
});
|
|
1051
1088
|
}
|
|
1089
|
+
invalidateTranscriptSource(sessionId) {
|
|
1090
|
+
this.transcriptResolutionCache.delete(sessionId);
|
|
1091
|
+
this.transcriptResolutionGeneration.set(sessionId, (this.transcriptResolutionGeneration.get(sessionId) ?? 0) + 1);
|
|
1092
|
+
}
|
|
1052
1093
|
async resolveTranscriptCandidates(sessionId) {
|
|
1053
1094
|
return this.transcriptCandidateInflight.run(sessionId, async () => {
|
|
1054
1095
|
const result = await this.resolveTranscriptCandidatesUncached(sessionId);
|
|
@@ -1119,7 +1160,13 @@ export class TmuxPtyAdapter {
|
|
|
1119
1160
|
candidates: [...candidates.values()].slice(0, TRANSCRIPT_CANDIDATE_LIMIT)
|
|
1120
1161
|
};
|
|
1121
1162
|
}
|
|
1122
|
-
async resolveTranscriptSourceUncached(sessionId) {
|
|
1163
|
+
async resolveTranscriptSourceUncached(sessionId, fresh = false) {
|
|
1164
|
+
const state = { daemonAmbiguous: false };
|
|
1165
|
+
const resolved = await this.resolveTranscriptSourceUncachedInner(sessionId, fresh, state);
|
|
1166
|
+
this.noteTranscriptAmbiguity(sessionId, !resolved && state.daemonAmbiguous);
|
|
1167
|
+
return resolved;
|
|
1168
|
+
}
|
|
1169
|
+
async resolveTranscriptSourceUncachedInner(sessionId, fresh, state) {
|
|
1123
1170
|
const panePid = this.panePidBySession.get(sessionId) ?? await this.readPanePidAsync(sessionId);
|
|
1124
1171
|
if (panePid === null) {
|
|
1125
1172
|
return null;
|
|
@@ -1140,6 +1187,7 @@ export class TmuxPtyAdapter {
|
|
|
1140
1187
|
}
|
|
1141
1188
|
});
|
|
1142
1189
|
let entries = [];
|
|
1190
|
+
const pendingCodexDepths = [];
|
|
1143
1191
|
let claudeSessionPending = false;
|
|
1144
1192
|
for (const [pid, args] of argsByPid) {
|
|
1145
1193
|
const processDepth = processDepthByPid.get(pid);
|
|
@@ -1170,8 +1218,10 @@ export class TmuxPtyAdapter {
|
|
|
1170
1218
|
entries.push({ candidate: live.source, rank: 'live', processDepth });
|
|
1171
1219
|
continue;
|
|
1172
1220
|
}
|
|
1173
|
-
if (live.status === 'pending' || live.status === 'ambiguous')
|
|
1174
|
-
|
|
1221
|
+
if (live.status === 'pending' || live.status === 'ambiguous') {
|
|
1222
|
+
pendingCodexDepths.push(processDepth);
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1175
1225
|
// Launcher pids (the npm node wrapper) merely relay the binary's
|
|
1176
1226
|
// argv; after /new their stale `resume <id>` args would conflict
|
|
1177
1227
|
// with the live session the binary itself reports, so only the
|
|
@@ -1181,13 +1231,21 @@ export class TmuxPtyAdapter {
|
|
|
1181
1231
|
}
|
|
1182
1232
|
// A connected daemon is authoritative, even when its current
|
|
1183
1233
|
// thread is pending/ambiguous. Never replace that with stale argv.
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1186
|
-
|
|
1234
|
+
const daemon = await lookupCodexTranscriptFromDaemon(pid, fresh ? { bypassCache: true } : undefined);
|
|
1235
|
+
const lookup = daemon.status === 'absent' || daemon.status === 'unavailable'
|
|
1236
|
+
? await lookupCodexTranscriptFromProcessLogs(pid) : daemon;
|
|
1237
|
+
if (lookup.status === 'pending' || lookup.status === 'ambiguous') {
|
|
1238
|
+
if (daemon.status === 'ambiguous')
|
|
1239
|
+
state.daemonAmbiguous = true;
|
|
1240
|
+
pendingCodexDepths.push(processDepth);
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1187
1243
|
const stated = lookup.status === 'matched' ? lookup.source
|
|
1188
1244
|
: lookup.status === 'absent' ? extractCodexArgsCandidate(args) : null;
|
|
1189
1245
|
if (stated) {
|
|
1190
|
-
|
|
1246
|
+
const client = daemon.status === 'matched' ? await describeTranscriptClient(pid, args) : null;
|
|
1247
|
+
entries.push({ candidate: stated, rank: daemon.status === 'matched' ? 'daemon' : 'stated', processDepth,
|
|
1248
|
+
...(client ? { client } : {}) });
|
|
1191
1249
|
}
|
|
1192
1250
|
}
|
|
1193
1251
|
}
|
|
@@ -1198,15 +1256,23 @@ export class TmuxPtyAdapter {
|
|
|
1198
1256
|
// other session's file) is wrong by construction.
|
|
1199
1257
|
entries = entries.filter((entry) => entry.candidate.provider !== 'claude' || entry.rank === 'live');
|
|
1200
1258
|
}
|
|
1201
|
-
|
|
1202
|
-
|
|
1259
|
+
// Uncertain child jobs cannot veto a closer write-open TUI owner. At
|
|
1260
|
+
// the same or shallower depth uncertainty still blocks all fallback.
|
|
1261
|
+
const closestLiveCodex = Math.min(...entries.filter(entry => entry.rank === 'live' && entry.candidate.provider === 'codex')
|
|
1262
|
+
.map(entry => entry.processDepth ?? Infinity));
|
|
1263
|
+
if (pendingCodexDepths.some(depth => depth <= closestLiveCodex))
|
|
1264
|
+
return null;
|
|
1265
|
+
const selected = selectTranscriptCandidateEntry(entries);
|
|
1266
|
+
if (!selected) {
|
|
1203
1267
|
return null;
|
|
1204
1268
|
}
|
|
1269
|
+
const candidate = selected.candidate;
|
|
1205
1270
|
const matches = candidate.filePath ? [candidate.filePath] : await findTranscriptFilesAsync(candidate.provider, candidate.sessionId);
|
|
1206
1271
|
if (matches.length !== 1) {
|
|
1207
1272
|
return null;
|
|
1208
1273
|
}
|
|
1209
|
-
return { provider: candidate.provider, sessionId: candidate.sessionId, filePath: matches[0]
|
|
1274
|
+
return { provider: candidate.provider, sessionId: candidate.sessionId, filePath: matches[0], evidence: selected.rank,
|
|
1275
|
+
...(selected.client ? { client: selected.client } : {}) };
|
|
1210
1276
|
}
|
|
1211
1277
|
async readPaneMetricsAsync(sessionId) {
|
|
1212
1278
|
try {
|
|
@@ -1296,6 +1362,10 @@ export class TmuxPtyAdapter {
|
|
|
1296
1362
|
return false;
|
|
1297
1363
|
}
|
|
1298
1364
|
}
|
|
1365
|
+
getSessionPid(sessionId) {
|
|
1366
|
+
const cached = this.panePidBySession.get(sessionId);
|
|
1367
|
+
return cached ? Promise.resolve(cached) : this.readPanePidAsync(sessionId);
|
|
1368
|
+
}
|
|
1299
1369
|
async readPanePidAsync(sessionId) {
|
|
1300
1370
|
try {
|
|
1301
1371
|
const result = await this.commandRunner('tmux', [
|
|
@@ -1718,6 +1788,17 @@ async function readProcessArgsAsync(pid) {
|
|
|
1718
1788
|
return [];
|
|
1719
1789
|
}
|
|
1720
1790
|
}
|
|
1791
|
+
// Launch facts for the TUI behind a daemon association. Missing facts stay
|
|
1792
|
+
// missing (never guessed), so corroboration fails closed.
|
|
1793
|
+
async function describeTranscriptClient(pid, args) {
|
|
1794
|
+
const startTicks = await readProcessStartTicksAsync(pid);
|
|
1795
|
+
if (startTicks === null)
|
|
1796
|
+
return null;
|
|
1797
|
+
const startedAtSeconds = await processStartTicksToSecondsAsync(startTicks);
|
|
1798
|
+
const stated = extractCodexArgsCandidate(args)?.sessionId;
|
|
1799
|
+
return { pid, startTicks, startedAtMs: startedAtSeconds === null ? null : Math.round(startedAtSeconds * 1000),
|
|
1800
|
+
...(stated ? { statedSessionId: stated } : {}) };
|
|
1801
|
+
}
|
|
1721
1802
|
function extractCodexArgsCandidate(args) {
|
|
1722
1803
|
const codexIndex = findCommandArg(args, 'codex');
|
|
1723
1804
|
if (codexIndex === -1) {
|
|
@@ -1826,7 +1907,10 @@ function hasCodexDescendant(tree, root, argsByPid) {
|
|
|
1826
1907
|
// tier returns null — falling back to a lower tier would resolve precisely
|
|
1827
1908
|
// when the best information says the answer is uncertain.
|
|
1828
1909
|
export function selectTranscriptCandidate(entries) {
|
|
1829
|
-
|
|
1910
|
+
return selectTranscriptCandidateEntry(entries)?.candidate ?? null;
|
|
1911
|
+
}
|
|
1912
|
+
function selectTranscriptCandidateEntry(entries) {
|
|
1913
|
+
for (const rank of ['live', 'daemon', 'stated', 'inferred']) {
|
|
1830
1914
|
let pool = entries.filter((entry) => entry.rank === rank);
|
|
1831
1915
|
if (pool.length === 0) {
|
|
1832
1916
|
continue;
|
|
@@ -1852,7 +1936,7 @@ export function selectTranscriptCandidate(entries) {
|
|
|
1852
1936
|
if (unique.size !== 1) {
|
|
1853
1937
|
return null;
|
|
1854
1938
|
}
|
|
1855
|
-
return [...unique.values()][0];
|
|
1939
|
+
return { candidate: [...unique.values()][0], rank };
|
|
1856
1940
|
}
|
|
1857
1941
|
return null;
|
|
1858
1942
|
}
|