aicodeman 1.45.0 → 1.46.0
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/README.md +26 -6
- package/dist/agent-codex-identity.d.ts +26 -0
- package/dist/agent-codex-identity.d.ts.map +1 -0
- package/dist/agent-codex-identity.js +253 -0
- package/dist/agent-codex-identity.js.map +1 -0
- package/dist/agent-inspection.d.ts +32 -0
- package/dist/agent-inspection.d.ts.map +1 -0
- package/dist/agent-inspection.js +196 -0
- package/dist/agent-inspection.js.map +1 -0
- package/dist/agent-staging.d.ts +130 -0
- package/dist/agent-staging.d.ts.map +1 -0
- package/dist/agent-staging.js +312 -0
- package/dist/agent-staging.js.map +1 -0
- package/dist/cli-agent.d.ts +31 -2
- package/dist/cli-agent.d.ts.map +1 -1
- package/dist/cli-agent.js +279 -34
- package/dist/cli-agent.js.map +1 -1
- package/dist/cli-history.d.ts +20 -0
- package/dist/cli-history.d.ts.map +1 -0
- package/dist/cli-history.js +61 -0
- package/dist/cli-history.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -1
- package/dist/cli.js.map +1 -1
- package/dist/codex-rollout-format.d.ts +66 -0
- package/dist/codex-rollout-format.d.ts.map +1 -0
- package/dist/codex-rollout-format.js +111 -0
- package/dist/codex-rollout-format.js.map +1 -0
- package/dist/codex-transcript.d.ts +3 -0
- package/dist/codex-transcript.d.ts.map +1 -1
- package/dist/codex-transcript.js +13 -41
- package/dist/codex-transcript.js.map +1 -1
- package/dist/config/cli-registry/schema.d.ts +12 -7
- package/dist/config/cli-registry/schema.d.ts.map +1 -1
- package/dist/config/cli-registry/schema.js +26 -0
- package/dist/config/cli-registry/schema.js.map +1 -1
- package/dist/config/cli-registry/stock.d.ts.map +1 -1
- package/dist/config/cli-registry/stock.js +40 -1
- package/dist/config/cli-registry/stock.js.map +1 -1
- package/dist/config/cli-registry/types.d.ts +57 -0
- package/dist/config/cli-registry/types.d.ts.map +1 -1
- package/dist/config/server-timing.d.ts +6 -0
- package/dist/config/server-timing.d.ts.map +1 -1
- package/dist/config/server-timing.js +9 -0
- package/dist/config/server-timing.js.map +1 -1
- package/dist/daemon-control.d.ts +1 -0
- package/dist/daemon-control.d.ts.map +1 -1
- package/dist/daemon-control.js +2 -0
- package/dist/daemon-control.js.map +1 -1
- package/dist/deepseek-transcript.d.ts +9 -7
- package/dist/deepseek-transcript.d.ts.map +1 -1
- package/dist/deepseek-transcript.js +9 -7
- package/dist/deepseek-transcript.js.map +1 -1
- package/dist/docker-hosts.d.ts +8 -0
- package/dist/docker-hosts.d.ts.map +1 -1
- package/dist/docker-hosts.js +29 -0
- package/dist/docker-hosts.js.map +1 -1
- package/dist/git-fetch-scheduler.d.ts +124 -0
- package/dist/git-fetch-scheduler.d.ts.map +1 -0
- package/dist/git-fetch-scheduler.js +249 -0
- package/dist/git-fetch-scheduler.js.map +1 -0
- package/dist/git-fetch.d.ts +241 -0
- package/dist/git-fetch.d.ts.map +1 -0
- package/dist/git-fetch.js +392 -0
- package/dist/git-fetch.js.map +1 -0
- package/dist/git-workspace-status.d.ts +36 -1
- package/dist/git-workspace-status.d.ts.map +1 -1
- package/dist/git-workspace-status.js +42 -5
- package/dist/git-workspace-status.js.map +1 -1
- package/dist/hooks-config.d.ts +14 -2
- package/dist/hooks-config.d.ts.map +1 -1
- package/dist/hooks-config.js +39 -3
- package/dist/hooks-config.js.map +1 -1
- package/dist/local-history.d.ts +71 -0
- package/dist/local-history.d.ts.map +1 -0
- package/dist/local-history.js +750 -0
- package/dist/local-history.js.map +1 -0
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +2 -2
- package/dist/mcp-server.js.map +1 -1
- package/dist/mux-interface.d.ts +5 -2
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/prompt-cache-keepwarm.d.ts.map +1 -1
- package/dist/prompt-cache-keepwarm.js +5 -3
- package/dist/prompt-cache-keepwarm.js.map +1 -1
- package/dist/session.d.ts +113 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +341 -13
- package/dist/session.js.map +1 -1
- package/dist/terminal-input-classifier.d.ts +8 -0
- package/dist/terminal-input-classifier.d.ts.map +1 -0
- package/dist/terminal-input-classifier.js +60 -0
- package/dist/terminal-input-classifier.js.map +1 -0
- package/dist/tmux-manager.d.ts +23 -1
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +54 -6
- package/dist/tmux-manager.js.map +1 -1
- package/dist/types/session.d.ts +15 -0
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/web/network-auth-policy.d.ts +13 -3
- package/dist/web/network-auth-policy.d.ts.map +1 -1
- package/dist/web/network-auth-policy.js +15 -3
- package/dist/web/network-auth-policy.js.map +1 -1
- package/dist/web/ports/infra-port.d.ts +2 -0
- package/dist/web/ports/infra-port.d.ts.map +1 -1
- package/dist/web/public/admin-ui.js.gz +0 -0
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/app.3093527f.js +43 -0
- package/dist/web/public/app.3093527f.js.br +0 -0
- package/dist/web/public/app.3093527f.js.gz +0 -0
- package/dist/web/public/approvals-ui.js.gz +0 -0
- package/dist/web/public/{constants.0b3cdde9.js → constants.affcdde6.js} +611 -12
- package/dist/web/public/constants.affcdde6.js.br +0 -0
- package/dist/web/public/constants.affcdde6.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/entrance-animations.js.gz +0 -0
- package/dist/web/public/git-status-ui.js +88 -5
- package/dist/web/public/git-status-ui.js.br +0 -0
- package/dist/web/public/git-status-ui.js.gz +0 -0
- package/dist/web/public/header-divider.eb2d2463.js.gz +0 -0
- package/dist/web/public/home-sessions.js.gz +0 -0
- package/dist/web/public/host-wake-ui.js.gz +0 -0
- package/dist/web/public/i18n.70265257.js +2 -0
- package/dist/web/public/i18n.70265257.js.br +0 -0
- package/dist/web/public/i18n.70265257.js.gz +0 -0
- package/dist/web/public/image-input.941386e7.js.gz +0 -0
- package/dist/web/public/index.html +107 -16
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.4e8b20ac.js.gz +0 -0
- package/dist/web/public/{mobile-handlers.1c0cbcd8.js → mobile-handlers.f7c3f692.js} +22 -0
- package/dist/web/public/mobile-handlers.f7c3f692.js.br +0 -0
- package/dist/web/public/mobile-handlers.f7c3f692.js.gz +0 -0
- package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
- package/dist/web/public/mobile-overview.js.gz +0 -0
- package/dist/web/public/mobile.1bbd2d67.css +1 -0
- package/dist/web/public/mobile.1bbd2d67.css.br +0 -0
- package/dist/web/public/mobile.1bbd2d67.css.gz +0 -0
- package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.78c3defb.js → panels-ui.4a56ee63.js} +1 -1
- package/dist/web/public/panels-ui.4a56ee63.js.br +0 -0
- package/dist/web/public/panels-ui.4a56ee63.js.gz +0 -0
- package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
- package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
- package/dist/web/public/readmymind-ui.js.gz +0 -0
- package/dist/web/public/reboot-restore-ui.js.gz +0 -0
- package/dist/web/public/respawn-ui.d7adcf6a.js.gz +0 -0
- package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
- package/dist/web/public/session-lineage.js.gz +0 -0
- package/dist/web/public/{session-ui.ea6b1994.js → session-ui.186f9419.js} +8 -8
- package/dist/web/public/session-ui.186f9419.js.br +0 -0
- package/dist/web/public/session-ui.186f9419.js.gz +0 -0
- package/dist/web/public/{settings-ui.085dd202.js → settings-ui.a191154e.js} +11 -11
- package/dist/web/public/settings-ui.a191154e.js.br +0 -0
- package/dist/web/public/settings-ui.a191154e.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
- package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
- package/dist/web/public/styles.7238eae8.css +1 -0
- package/dist/web/public/styles.7238eae8.css.br +0 -0
- package/dist/web/public/styles.7238eae8.css.gz +0 -0
- package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
- package/dist/web/public/sw.js +2 -2
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/tab-layout-browser.2d6189f7.js.gz +0 -0
- package/dist/web/public/{tab-rail-resize.7157e0bd.js → tab-rail-resize.b5cdceaf.js} +1 -1
- package/dist/web/public/tab-rail-resize.b5cdceaf.js.br +0 -0
- package/dist/web/public/tab-rail-resize.b5cdceaf.js.gz +0 -0
- package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
- package/dist/web/public/terminal-split.js.gz +0 -0
- package/dist/web/public/terminal-tile.js +2 -0
- package/dist/web/public/terminal-tile.js.br +0 -0
- package/dist/web/public/terminal-tile.js.gz +0 -0
- package/dist/web/public/terminal-ui.7e787829.js +4 -0
- package/dist/web/public/terminal-ui.7e787829.js.br +0 -0
- package/dist/web/public/terminal-ui.7e787829.js.gz +0 -0
- package/dist/web/public/tile-grid.js.gz +0 -0
- package/dist/web/public/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
- package/dist/web/public/vendor/fflate.min.js.gz +0 -0
- package/dist/web/public/vendor/marked.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/{xterm-predictive-echo.ae5e3d5d.js → xterm-predictive-echo.276b11d9.js} +1 -1
- package/dist/web/public/vendor/xterm-predictive-echo.276b11d9.js.br +0 -0
- package/dist/web/public/vendor/xterm-predictive-echo.276b11d9.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.1be42307.js +5 -0
- package/dist/web/public/vendor/xterm-zerolag-input.1be42307.js.br +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.1be42307.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
- package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
- package/dist/web/public/webview-tabs.js.gz +0 -0
- package/dist/web/public/whats-new.json +9 -9
- package/dist/web/routes/agent-session-routes.d.ts +20 -0
- package/dist/web/routes/agent-session-routes.d.ts.map +1 -0
- package/dist/web/routes/agent-session-routes.js +160 -0
- package/dist/web/routes/agent-session-routes.js.map +1 -0
- package/dist/web/routes/case-routes.d.ts.map +1 -1
- package/dist/web/routes/case-routes.js +32 -26
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/config-backup-routes.js +2 -2
- package/dist/web/routes/config-backup-routes.js.map +1 -1
- package/dist/web/routes/git-status-routes.d.ts +21 -7
- package/dist/web/routes/git-status-routes.d.ts.map +1 -1
- package/dist/web/routes/git-status-routes.js +78 -12
- package/dist/web/routes/git-status-routes.js.map +1 -1
- package/dist/web/routes/index.d.ts +1 -0
- package/dist/web/routes/index.d.ts.map +1 -1
- package/dist/web/routes/index.js +1 -0
- package/dist/web/routes/index.js.map +1 -1
- package/dist/web/routes/ralph-routes.d.ts.map +1 -1
- package/dist/web/routes/ralph-routes.js +7 -16
- package/dist/web/routes/ralph-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +96 -28
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/routes/system-routes.d.ts.map +1 -1
- package/dist/web/routes/system-routes.js +84 -57
- package/dist/web/routes/system-routes.js.map +1 -1
- package/dist/web/routes/ws-routes.d.ts +5 -0
- package/dist/web/routes/ws-routes.d.ts.map +1 -1
- package/dist/web/routes/ws-routes.js +84 -47
- package/dist/web/routes/ws-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +30 -7
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +40 -1
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/server.d.ts +5 -2
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +84 -14
- package/dist/web/server.js.map +1 -1
- package/dist/web/settings-writer.d.ts +47 -0
- package/dist/web/settings-writer.d.ts.map +1 -0
- package/dist/web/settings-writer.js +110 -0
- package/dist/web/settings-writer.js.map +1 -0
- package/package.json +1 -1
- package/skills/codeman/SKILL.md +25 -6
- package/skills/codeman/reference/history.md +84 -0
- package/skills/codeman/reference/inspection-and-staging.md +152 -0
- package/skills/codeman/reference/messaging.md +1 -1
- package/skills/codeman/reference/verbs.md +18 -3
- package/dist/web/public/app.42d0abae.js +0 -43
- package/dist/web/public/app.42d0abae.js.br +0 -0
- package/dist/web/public/app.42d0abae.js.gz +0 -0
- package/dist/web/public/constants.0b3cdde9.js.br +0 -0
- package/dist/web/public/constants.0b3cdde9.js.gz +0 -0
- package/dist/web/public/i18n.1e68220d.js +0 -2
- package/dist/web/public/i18n.1e68220d.js.br +0 -0
- package/dist/web/public/i18n.1e68220d.js.gz +0 -0
- package/dist/web/public/mobile-handlers.1c0cbcd8.js.br +0 -0
- package/dist/web/public/mobile-handlers.1c0cbcd8.js.gz +0 -0
- package/dist/web/public/mobile.12fb18de.css +0 -1
- package/dist/web/public/mobile.12fb18de.css.br +0 -0
- package/dist/web/public/mobile.12fb18de.css.gz +0 -0
- package/dist/web/public/panels-ui.78c3defb.js.br +0 -0
- package/dist/web/public/panels-ui.78c3defb.js.gz +0 -0
- package/dist/web/public/session-ui.ea6b1994.js.br +0 -0
- package/dist/web/public/session-ui.ea6b1994.js.gz +0 -0
- package/dist/web/public/settings-ui.085dd202.js.br +0 -0
- package/dist/web/public/settings-ui.085dd202.js.gz +0 -0
- package/dist/web/public/styles.3b59b41d.css +0 -1
- package/dist/web/public/styles.3b59b41d.css.br +0 -0
- package/dist/web/public/styles.3b59b41d.css.gz +0 -0
- package/dist/web/public/tab-rail-resize.7157e0bd.js.br +0 -0
- package/dist/web/public/tab-rail-resize.7157e0bd.js.gz +0 -0
- package/dist/web/public/terminal-ui.fb2a27f5.js +0 -2
- package/dist/web/public/terminal-ui.fb2a27f5.js.br +0 -0
- package/dist/web/public/terminal-ui.fb2a27f5.js.gz +0 -0
- package/dist/web/public/vendor/xterm-predictive-echo.ae5e3d5d.js.br +0 -0
- package/dist/web/public/vendor/xterm-predictive-echo.ae5e3d5d.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js +0 -4
- package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.br +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.gz +0 -0
|
@@ -2165,12 +2165,15 @@ const RELATIVE_FILE_PATH_LINK_PATTERN =
|
|
|
2165
2165
|
const FILE_LINK_EXTENSION_GROUP = /\\\.(\(\?:[^()]*\))\)\\b$/.exec(FILE_PATH_LINK_PATTERN.source)[1];
|
|
2166
2166
|
|
|
2167
2167
|
/**
|
|
2168
|
-
* Every linkable file path in `text`,
|
|
2169
|
-
*
|
|
2170
|
-
*
|
|
2168
|
+
* Every linkable file path in `text`, sorted by position. Spaced candidates
|
|
2169
|
+
* reserve their whole span first, even when unlinkable; rooted, `~/`, relative
|
|
2170
|
+
* and folder detectors then skip reserved spans, earlier matches and URLs.
|
|
2171
2171
|
*
|
|
2172
2172
|
* The one entry point for both consumers (terminal link provider, response
|
|
2173
|
-
* viewer)
|
|
2173
|
+
* viewer). The viewer carries a leading bold label into the text it passes
|
|
2174
|
+
* (spacedFilePathOnLine judges it, as for the same line in the terminal) and
|
|
2175
|
+
* reads a text node that follows another inline element as mid-line; paths
|
|
2176
|
+
* split across inline elements are still detected per text node.
|
|
2174
2177
|
*
|
|
2175
2178
|
* @param {string} text
|
|
2176
2179
|
* @returns {Array<{path: string, index: number, folder?: true}>} `index` is where `path` starts in
|
|
@@ -2184,32 +2187,154 @@ function findFilePathLinks(text) {
|
|
|
2184
2187
|
// `?img=b.png` belong to the URL link, not to a second, overlapping file link.
|
|
2185
2188
|
// (`file:///home/x.png` is not one of them: its path is the file.)
|
|
2186
2189
|
const urls = [];
|
|
2187
|
-
const
|
|
2190
|
+
const webUrlPattern = /\bhttps?:\/\/\S+/gi;
|
|
2188
2191
|
let match;
|
|
2189
|
-
while ((match =
|
|
2192
|
+
while ((match = webUrlPattern.exec(value)) !== null) urls.push({ index: match.index, path: match[0] });
|
|
2190
2193
|
const inUrl = (index, length) => urls.some((u) => index < u.index + u.path.length && u.index < index + length);
|
|
2191
2194
|
|
|
2195
|
+
// A path with spaces is taken whole, or the span it covers links nothing
|
|
2196
|
+
// (spacedFilePathOnLine). Either way no detector below may cut a fragment out of it.
|
|
2197
|
+
const reserved = [];
|
|
2198
|
+
let lineStart = 0;
|
|
2199
|
+
for (const line of value.split('\n')) {
|
|
2200
|
+
const spaced = spacedFilePathOnLine(line);
|
|
2201
|
+
if (spaced && !inUrl(lineStart + spaced.index, spaced.path.length)) {
|
|
2202
|
+
const index = lineStart + spaced.index;
|
|
2203
|
+
reserved.push({ index, path: spaced.path });
|
|
2204
|
+
if (spaced.link) found.push({ path: spaced.path, index });
|
|
2205
|
+
}
|
|
2206
|
+
lineStart += line.length + 1;
|
|
2207
|
+
}
|
|
2208
|
+
const isReserved = (index, length) =>
|
|
2209
|
+
reserved.some((r) => index < r.index + r.path.length && r.index < index + length);
|
|
2210
|
+
|
|
2192
2211
|
const absolute = absoluteFilePathPattern();
|
|
2193
2212
|
while ((match = absolute.exec(value)) !== null) {
|
|
2194
|
-
if (!inUrl(match.index, match[1].length)
|
|
2213
|
+
if (!inUrl(match.index, match[1].length) && !isReserved(match.index, match[1].length)) {
|
|
2214
|
+
found.push({ path: match[1], index: match.index });
|
|
2215
|
+
}
|
|
2195
2216
|
}
|
|
2196
2217
|
const relative = new RegExp(RELATIVE_FILE_PATH_LINK_PATTERN.source, 'g');
|
|
2197
2218
|
while ((match = relative.exec(value)) !== null) {
|
|
2198
2219
|
const path = match[2];
|
|
2199
2220
|
const index = match.index + match[1].length;
|
|
2200
|
-
if (overlaps(index, path.length) || inUrl(index, path.length)) continue;
|
|
2221
|
+
if (overlaps(index, path.length) || inUrl(index, path.length) || isReserved(index, path.length)) continue;
|
|
2201
2222
|
found.push({ path, index });
|
|
2202
2223
|
}
|
|
2203
2224
|
const folders = new RegExp(FOLDER_PATH_LINK_PATTERN.source, 'g');
|
|
2204
2225
|
while ((match = folders.exec(value)) !== null) {
|
|
2205
2226
|
const path = folderLinkPath(match[2]);
|
|
2206
2227
|
const index = match.index + match[1].length;
|
|
2207
|
-
if (!path || overlaps(index, path.length) || inUrl(index, path.length)) continue;
|
|
2228
|
+
if (!path || overlaps(index, path.length) || inUrl(index, path.length) || isReserved(index, path.length)) continue;
|
|
2208
2229
|
found.push({ path, index, folder: true });
|
|
2209
2230
|
}
|
|
2210
2231
|
return found.sort((a, b) => a.index - b.index);
|
|
2211
2232
|
}
|
|
2212
2233
|
|
|
2234
|
+
/** Labels that explicitly identify a path, for {@link spacedFilePathOnLine}. */
|
|
2235
|
+
const SPACED_FILE_PATH_LABEL_PATTERN =
|
|
2236
|
+
/^(?:path|files?|saved (?:to|as)|wrote|written to|output|created|location|attachment):[ \t]*$/i;
|
|
2237
|
+
|
|
2238
|
+
/**
|
|
2239
|
+
* A file path WITH SPACES on one line, or null.
|
|
2240
|
+
*
|
|
2241
|
+
* Every path pattern ends a path at whitespace, so `/mnt/c/OneDrive - Contoso/a.docx`
|
|
2242
|
+
* linked `/mnt/c/OneDrive` plus a relative `Contoso/a.docx`, two links to the
|
|
2243
|
+
* wrong targets. A space is only safe inside a path where the line itself bounds
|
|
2244
|
+
* the path, so a spaced path is recognised in exactly these shapes:
|
|
2245
|
+
*
|
|
2246
|
+
* /mnt/c/My Docs/report.docx the whole line is the path
|
|
2247
|
+
* - /mnt/c/My Docs/report.docx after a list marker (`-`, `*`, `•`, `1.`, `2)`)
|
|
2248
|
+
* Saved to: 03 Attachments/Build SIPOC Design/plan.pptx after a path label (SPACED_FILE_PATH_LABEL_PATTERN)
|
|
2249
|
+
*
|
|
2250
|
+
* An absolute path may stand alone or follow a marker. Any label, before an
|
|
2251
|
+
* absolute, relative or `~/` path, must be an explicit path label
|
|
2252
|
+
* (SPACED_FILE_PATH_LABEL_PATTERN), with an optional marker before it: generic
|
|
2253
|
+
* labels (`Error:`, `Note:`) and bare bullets also introduce prose, so a relative
|
|
2254
|
+
* path needs a path label to be taken spaced. One trailing sentence mark is left
|
|
2255
|
+
* out, and a leading run of `./` or `../` is not a segment. The candidate must read as ONE path:
|
|
2256
|
+
* it ends on a known extension, a rooted one has a directory before its first
|
|
2257
|
+
* space, no earlier word holds an extension (`a.md,`, `a.ts#L12`) or ends in `/`,
|
|
2258
|
+
* and none holds a `:` and a digit (`a.ts:42`, a line suffix ends the path). Later words cannot start a new
|
|
2259
|
+
* `/`, `~/`, `./` or `../` path, no earlier word ends in `:`, is an arrow or has
|
|
2260
|
+
* no letter or digit (a prompt glyph such as `❯` or `$`; a bare `-` is kept), and
|
|
2261
|
+
* a run of spaces (a column gap) is refused, so two paths on a line are never
|
|
2262
|
+
* merged. A relative candidate whose only `/` word is its last is prose after the
|
|
2263
|
+
* label (`Wrote: 120 lines to src/foo.ts`) and is left alone, which also splits
|
|
2264
|
+
* `Saved to: My Docs/plan.md` as master does. An earlier word that is a lowercase
|
|
2265
|
+
* connective or verb (SPACED_PATH_PROSE_WORD: `and`, `in`, `has`, `contains`, ...)
|
|
2266
|
+
* or a bare `=` makes the candidate a sentence (`/tmp/out has the logs in run.log`),
|
|
2267
|
+
* so a name such as `Pros and Cons.md` splits as master does. A command line is
|
|
2268
|
+
* refused: an executable under `bin/`, `.bin/`, `scripts/` or `tools/` as the
|
|
2269
|
+
* first word, any earlier word ending in `$` (a shell prompt, `/home/me/proj$ cat a.md`,
|
|
2270
|
+
* `/mnt/c/My Docs$ ls a.md`), or any `-x`/`--x` flag word.
|
|
2271
|
+
*
|
|
2272
|
+
* A candidate that is one path but not a linkable one (an unknown root such as
|
|
2273
|
+
* `/etc`, a `.`/`..`/empty segment) comes back with `link: false` when its last
|
|
2274
|
+
* word still holds a `/`: its span links nothing, rather than the fragment after
|
|
2275
|
+
* its last space. Otherwise (`/usr/bin/env node build.js`) the line is left to
|
|
2276
|
+
* the ordinary detectors.
|
|
2277
|
+
*
|
|
2278
|
+
* Linear: one bounded prefix match, then string scans.
|
|
2279
|
+
*
|
|
2280
|
+
* @param {string} line One line, no newline.
|
|
2281
|
+
* @returns {{path: string, index: number, link: boolean} | null}
|
|
2282
|
+
*/
|
|
2283
|
+
function spacedFilePathOnLine(line) {
|
|
2284
|
+
const text = String(line || '');
|
|
2285
|
+
const prefix = /^[ \t]*((?:\d{1,3}[.)]|[-*•])[ \t]+)?((?:[A-Za-z][\w-]{0,30}(?: [\w-]{1,30}){0,3}):[ \t]+)?/.exec(text);
|
|
2286
|
+
const index = prefix[0].length;
|
|
2287
|
+
const path = text.slice(index).trimEnd().replace(/[.,;:!?]$/, '');
|
|
2288
|
+
if (!path.includes(' ') || !path.includes('/') || path.length > 1000) return null;
|
|
2289
|
+
// After a label, only a path label introduces a path; with no label, only an
|
|
2290
|
+
// absolute path may stand alone. `Error: /a/b contains c.md` is prose.
|
|
2291
|
+
if (prefix[2] ? !SPACED_FILE_PATH_LABEL_PATTERN.test(prefix[2]) : !path.startsWith('/')) return null;
|
|
2292
|
+
if (!PATH_TOKEN_ENDS_WITH_EXTENSION.test(path)) return null;
|
|
2293
|
+
// A run of spaces is a column gap (`git status`, `ls -l`), not a name.
|
|
2294
|
+
if (/[\t"'`<>|;&\x00-\x1f\\]/.test(path) || path.includes('://') || path.includes(' ')) return null;
|
|
2295
|
+
const words = path.split(' ');
|
|
2296
|
+
// A rooted candidate must have a directory before its first space: `/usr/bin/env node a.js`
|
|
2297
|
+
// or `/review the changes in src/a.ts` is a command, not a spaced path.
|
|
2298
|
+
if (path.startsWith('/') && !/^\/[^/]+\//.test(words[0])) return null;
|
|
2299
|
+
// A command whose executable sits under a known root (`/opt/homebrew/bin/node a.js`,
|
|
2300
|
+
// `/home/me/proj/node_modules/.bin/tsc -p tsconfig.json`, `/home/me/proj/scripts/deploy prod.yml`)
|
|
2301
|
+
// is not a path either.
|
|
2302
|
+
if (/\/(?:\.?bin|scripts?|tools)\/[^/]+$/.test(words[0]) || words.some((word) => /^-{1,2}\w/.test(word))) {
|
|
2303
|
+
return null;
|
|
2304
|
+
}
|
|
2305
|
+
for (const word of words.slice(0, -1)) {
|
|
2306
|
+
if (word.endsWith('/') || word.endsWith(':') || /^(?:—|–|→|=>|->|<-|=)$/.test(word)) return null;
|
|
2307
|
+
// A shell prompt that prints its working directory in full (`/home/me/proj$ cat
|
|
2308
|
+
// notes.md`; WSL's `/mnt/c/...` is outside `$HOME`) is a command line too, and so is
|
|
2309
|
+
// one whose directory has a space in it (`/mnt/c/My Docs$ ls notes.md`): the `$`
|
|
2310
|
+
// can end any earlier word, not only the first.
|
|
2311
|
+
if (word.endsWith('$')) return null;
|
|
2312
|
+
// A word with no letter or digit (a prompt glyph `❯` or `$`, a `✓`) is never
|
|
2313
|
+
// part of a file name, apart from a bare `-` (`OneDrive - Contoso`).
|
|
2314
|
+
if (word !== '-' && !/[\p{L}\p{N}]/u.test(word)) return null;
|
|
2315
|
+
// `a.ts:42` (grep -n, compiler output): the path already ended at the line suffix.
|
|
2316
|
+
if (/:\d/.test(word)) return null;
|
|
2317
|
+
// An extension anywhere in an earlier word (`a.md,`, `a.ts#L12`) ends a path there.
|
|
2318
|
+
if (PATH_TOKEN_HAS_EXTENSION.test(word)) return null;
|
|
2319
|
+
// A connective or verb joins a path to prose (`/tmp/out has the logs in run.log`,
|
|
2320
|
+
// `Files: src/components and utils/x.ts`), so the candidate is a sentence.
|
|
2321
|
+
if (SPACED_PATH_PROSE_WORD.test(word)) return null;
|
|
2322
|
+
}
|
|
2323
|
+
if (words.slice(1).some((word) => /^(?:\/|~\/|\.{1,2}\/)/.test(word))) return null;
|
|
2324
|
+
const rooted = SPACED_PATH_ROOT.test(path) || path.startsWith('~/');
|
|
2325
|
+
const segments = path.replace(/^(?:~?\/|(?:\.\.?\/)+)/, '').split('/');
|
|
2326
|
+
const link = (rooted || !path.startsWith('/')) && segments.every((seg) => seg && seg !== '.' && seg !== '..');
|
|
2327
|
+
// A path label vouches that a path follows, not for the words before it: when
|
|
2328
|
+
// only the last word holds a `/` (`Wrote: 120 lines to src/foo.ts`), the spaces
|
|
2329
|
+
// are prose, so a relative candidate is left to the ordinary detectors.
|
|
2330
|
+
const firstSlash = words.findIndex((word) => word.includes('/'));
|
|
2331
|
+
if (link && firstSlash === words.length - 1 && !path.startsWith('/')) return null;
|
|
2332
|
+
// Reserve an unlinkable span only when it still reads as a path to its end;
|
|
2333
|
+
// otherwise leave the line to the ordinary detectors.
|
|
2334
|
+
if (!link && !words[words.length - 1].includes('/')) return null;
|
|
2335
|
+
return { path, index, link };
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2213
2338
|
/**
|
|
2214
2339
|
* Folder paths, for the folder grid: rooted like {@link FILE_PATH_LINK_PATTERN}
|
|
2215
2340
|
* (`/mnt/raid1/neon_getaway/styleframes`), `~/renders`, or relative with a
|
|
@@ -2344,8 +2469,16 @@ function terminalLogicalLine(buffer, row, cols, maxRows) {
|
|
|
2344
2469
|
const continuesPrevious = (r) => {
|
|
2345
2470
|
if (r <= 0) return false;
|
|
2346
2471
|
if (lineAt(r)?.isWrapped) return true;
|
|
2472
|
+
// A divider row never continues the row above, even when that row reaches
|
|
2473
|
+
// the right edge: a URL ending at the edge must not absorb `────` below it.
|
|
2474
|
+
if (/^[\s─━═╌╍┄┅┈┉-]+$/u.test(lineAt(r)?.translateToString(true) || '')) return false;
|
|
2347
2475
|
const prev = lineAt(r - 1);
|
|
2348
|
-
|
|
2476
|
+
const prevText = prev?.translateToString(true) || '';
|
|
2477
|
+
// A full-width table divider is a row boundary, not a hard-wrapped line.
|
|
2478
|
+
// Treating it as a continuation glues the divider onto the row below and
|
|
2479
|
+
// hides that row's wrapped URL from the link provider.
|
|
2480
|
+
if (/^[\s─━═╌╍┄┅┈┉-]+$/u.test(prevText)) return false;
|
|
2481
|
+
return prevText.length >= width;
|
|
2349
2482
|
};
|
|
2350
2483
|
|
|
2351
2484
|
let startRow = row;
|
|
@@ -2396,6 +2529,16 @@ const FILE_LINK_ROOT_GROUP = /^\(\\\/\(\?:([^()]*)\)/.exec(FILE_PATH_LINK_PATTER
|
|
|
2396
2529
|
const PATH_TOKEN_ENDS_WITH_EXTENSION = new RegExp(`\\.${FILE_LINK_EXTENSION_GROUP}$`);
|
|
2397
2530
|
const PATH_TOKEN_HAS_EXTENSION = new RegExp(`\\.${FILE_LINK_EXTENSION_GROUP}\\b`);
|
|
2398
2531
|
const PATH_TOKEN_STARTS_NEW_PATH = new RegExp(`^(?:~\\/|\\/(?:${FILE_LINK_ROOT_GROUP})\\/)`);
|
|
2532
|
+
/**
|
|
2533
|
+
* Lowercase connectives and verbs that put a spaced candidate in a sentence, for
|
|
2534
|
+
* {@link spacedFilePathOnLine}. `at` stays out (`Screenshot 2026-10-24 at 10.00.00.png`)
|
|
2535
|
+
* and so does `of` (`Copy of report.docx`); a name using a listed word
|
|
2536
|
+
* (`Pros and Cons.md`) splits as it did before spaced paths were linked.
|
|
2537
|
+
*/
|
|
2538
|
+
const SPACED_PATH_PROSE_WORD =
|
|
2539
|
+
/^(?:and|or|to|in|into|from|the|is|are|was|were|has|have|had|contains|holds|see|then|vs|via|as)$/;
|
|
2540
|
+
/** A rooted path's start (`/home/`, `/mnt/`, …), for {@link spacedFilePathOnLine}. */
|
|
2541
|
+
const SPACED_PATH_ROOT = new RegExp(`^\\/(?:${FILE_LINK_ROOT_GROUP})\\/`);
|
|
2399
2542
|
/** A relative head that names its own location (`./a`, `../a`) is an entry of its own. */
|
|
2400
2543
|
const PATH_TOKEN_STARTS_EXPLICIT_RELATIVE = /^\.{1,2}\//;
|
|
2401
2544
|
/** First directory of a path, a leading `/` or `./` ignored: `src` for `src/a.ts` and `/src/a.ts`. */
|
|
@@ -2519,6 +2662,395 @@ function terminalPathLine(buffer, row, cols, maxRows) {
|
|
|
2519
2662
|
return { startRow: parts[0].startRow, endRow: parts[parts.length - 1].endRow, text, offsetToCell, cellToOffset };
|
|
2520
2663
|
}
|
|
2521
2664
|
|
|
2665
|
+
/**
|
|
2666
|
+
* Web URLs in terminal text, with the trimming the link provider applies.
|
|
2667
|
+
*
|
|
2668
|
+
* A single `&` is part of the URL (query separator); `&&` is the shell operator
|
|
2669
|
+
* and ends it. `)` is kept INSIDE a URL when it closes a `(` that is also inside
|
|
2670
|
+
* it (`…/Team-Space(QA)Archive/…` used to link only as far as `(QA`), and an
|
|
2671
|
+
* unmatched `)` ends the URL, which keeps the closing paren of `(https://…)` and
|
|
2672
|
+
* `[text](https://…)` out of the link. Trailing sentence punctuation is trimmed.
|
|
2673
|
+
*
|
|
2674
|
+
* @param {string} text
|
|
2675
|
+
* @returns {Array<{url: string, startIndex: number, endIndex: number, rawEnd: number}>}
|
|
2676
|
+
* `endIndex` is exclusive; `rawEnd` is where the match ran to before the
|
|
2677
|
+
* paren/punctuation trim (the wrap-continuation test needs it).
|
|
2678
|
+
*/
|
|
2679
|
+
function findTerminalUrls(text) {
|
|
2680
|
+
// Named `urlPattern` on purpose: test/link-provider-regex.test.ts extracts it
|
|
2681
|
+
// by name to prove it stays linear-time on hostile lines.
|
|
2682
|
+
const urlPattern = /https?:\/\/(?:[^\s"'<>|;&\]\x00-\x1f]|&(?!&))+/g;
|
|
2683
|
+
const urls = [];
|
|
2684
|
+
let match;
|
|
2685
|
+
while ((match = urlPattern.exec(String(text || ''))) !== null) {
|
|
2686
|
+
const raw = match[0];
|
|
2687
|
+
let end = raw.length;
|
|
2688
|
+
let depth = 0;
|
|
2689
|
+
for (let i = 0; i < raw.length; i++) {
|
|
2690
|
+
if (raw[i] === '(') depth++;
|
|
2691
|
+
else if (raw[i] === ')') {
|
|
2692
|
+
if (depth === 0) {
|
|
2693
|
+
end = i;
|
|
2694
|
+
break;
|
|
2695
|
+
}
|
|
2696
|
+
depth--;
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
const url = raw.slice(0, end).replace(/[.,;:!?&]+$/, '');
|
|
2700
|
+
if (!/:\/\/[^/]/.test(url)) continue;
|
|
2701
|
+
urls.push({ url, startIndex: match.index, endIndex: match.index + url.length, rawEnd: match.index + raw.length });
|
|
2702
|
+
}
|
|
2703
|
+
return urls;
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
/**
|
|
2707
|
+
* The row span a URL covers when the CLI wrapped it with its OWN newlines at a
|
|
2708
|
+
* point short of the terminal width.
|
|
2709
|
+
*
|
|
2710
|
+
* {@link terminalLogicalLine} joins soft wraps and full-width hard wraps. A CLI
|
|
2711
|
+
* that word-wraps inside a table cell or a paragraph (codex's markdown tables,
|
|
2712
|
+
* for one) breaks a long URL at `/`, `?`, `-` … well before the right edge and
|
|
2713
|
+
* indents the rest to the cell's column, so neither rule fires and each row
|
|
2714
|
+
* linked a fragment. A row continues the URL above it only when ALL hold:
|
|
2715
|
+
* - the URL fragment ends on a character a wrapper breaks after
|
|
2716
|
+
* (`/ ? & = - _ % # + ~`); a following table column may be excluded;
|
|
2717
|
+
* - the next row is indented, but no further right than the column the URL
|
|
2718
|
+
* started in (a table cell or list item continuation, not unrelated text),
|
|
2719
|
+
* and no column gap lies between its indent and the URL on the first row
|
|
2720
|
+
* (that would be the next record of a table);
|
|
2721
|
+
* - that row's first token looks like URL material (contains one of
|
|
2722
|
+
* `% & = / ? . - _`), is not a bare number/version or list marker,
|
|
2723
|
+
* and is not itself a new `scheme://`, a shell prompt (`user@host:`),
|
|
2724
|
+
* a home path (`~/`), an email address, an HTTP status line, a label
|
|
2725
|
+
* ending in `:`, a measurement or clock time (`45%`, `12.3ms`,
|
|
2726
|
+
* `10:00:00`), or a `name=value` field under a URL ending in `/`;
|
|
2727
|
+
* without `% & = / ?`, the token must finish its cell (row end or a
|
|
2728
|
+
* gap of at least two spaces) and must not be code with a call paren
|
|
2729
|
+
* or a word ending a sentence (`Done.`).
|
|
2730
|
+
* The last rule is what stops prose such as "…/files/" + "then run it" from
|
|
2731
|
+
* being glued into one link.
|
|
2732
|
+
*
|
|
2733
|
+
* Bounded by `maxRows`, like the logical line. Pure: reads rows only.
|
|
2734
|
+
*
|
|
2735
|
+
* @param {{getLine: (row: number) => any, length: number}} buffer
|
|
2736
|
+
* @param {number} row 0-based absolute row being examined.
|
|
2737
|
+
* @param {number} cols Terminal width.
|
|
2738
|
+
* @param {number} [maxRows]
|
|
2739
|
+
* @returns {{startRow: number, endRow: number, firstRowUrlEnd?: number} | null}
|
|
2740
|
+
* `firstRowUrlEnd` is where the URL's cell ends on the first row when
|
|
2741
|
+
* other table columns follow it and that row does not soft-wrap; null
|
|
2742
|
+
* means the row is not part of a URL continued this way.
|
|
2743
|
+
*/
|
|
2744
|
+
function terminalUrlContinuationExtent(buffer, row, cols, maxRows) {
|
|
2745
|
+
const bound = Math.max(1, maxRows || 12);
|
|
2746
|
+
const here = terminalLogicalLine(buffer, row, cols, bound);
|
|
2747
|
+
if (!here) return null;
|
|
2748
|
+
|
|
2749
|
+
const chainFrom = (start) => {
|
|
2750
|
+
const first = terminalLogicalLine(buffer, start, cols, bound);
|
|
2751
|
+
if (!first || first.startRow !== start) return null;
|
|
2752
|
+
let text = first.text.replace(/\s+$/, '');
|
|
2753
|
+
let endRow = first.endRow;
|
|
2754
|
+
let urlStartCol = -1;
|
|
2755
|
+
let firstRowUrlEnd = null;
|
|
2756
|
+
let extended = false;
|
|
2757
|
+
while (endRow - start + 1 < bound) {
|
|
2758
|
+
const urls = findTerminalUrls(text);
|
|
2759
|
+
const last = urls[urls.length - 1];
|
|
2760
|
+
// Only on a one-row first line: firstRowUrlEnd cuts the first PHYSICAL
|
|
2761
|
+
// row (terminalLinkLine), so a column sitting on the soft-wrapped part of
|
|
2762
|
+
// the URL's row would stay in the text and take the next row with it.
|
|
2763
|
+
const trailingColumns =
|
|
2764
|
+
first.startRow === first.endRow &&
|
|
2765
|
+
endRow === first.endRow &&
|
|
2766
|
+
last &&
|
|
2767
|
+
last.rawEnd < text.length &&
|
|
2768
|
+
/^ {2,}\S/.test(text.slice(last.rawEnd));
|
|
2769
|
+
const urlText = trailingColumns ? text.slice(0, last.rawEnd) : text;
|
|
2770
|
+
if (!last || last.rawEnd !== urlText.length || !/[/?&=\-_%#+~]$/.test(urlText)) break;
|
|
2771
|
+
if (urlStartCol < 0) urlStartCol = first.offsetToCell(last.startIndex).col;
|
|
2772
|
+
const next = buffer.getLine(endRow + 1);
|
|
2773
|
+
if (!next || next.isWrapped) break;
|
|
2774
|
+
const nextText = next.translateToString(true) || '';
|
|
2775
|
+
const indent = nextText.length - nextText.replace(/^\s+/, '').length;
|
|
2776
|
+
const token = nextText.slice(indent).split(/\s/, 1)[0];
|
|
2777
|
+
// A hard continuation is indented (a table cell, a list item, Claude's
|
|
2778
|
+
// two-space paragraph indent); a column-0 row is a new line of output.
|
|
2779
|
+
if (!token || !indent || indent > urlStartCol || /^(?:\d+[.)]|[-*•])(?:\s|$)/.test(nextText.slice(indent)))
|
|
2780
|
+
break;
|
|
2781
|
+
// When other table columns follow the URL, the next row must align with
|
|
2782
|
+
// the start of its cell. A new numbered row must not become URL text.
|
|
2783
|
+
if (
|
|
2784
|
+
trailingColumns &&
|
|
2785
|
+
(!indent ||
|
|
2786
|
+
first.text[indent - 1] !== ' ' ||
|
|
2787
|
+
first.text[indent] === ' ' ||
|
|
2788
|
+
/ {2,}/.test(first.text.slice(indent, last.startIndex)))
|
|
2789
|
+
)
|
|
2790
|
+
break;
|
|
2791
|
+
// The next row must start inside the URL's own cell: a column gap between
|
|
2792
|
+
// its indent and the URL on the first row means it opens an earlier
|
|
2793
|
+
// column (the next record of a table), not the rest of the URL.
|
|
2794
|
+
if (/ {2,}/.test(first.text.slice(indent, last.startIndex).trimStart())) break;
|
|
2795
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(token) || !/^[A-Za-z0-9%_~]/.test(token)) break;
|
|
2796
|
+
// A shell prompt (`user@host:~/dir$`), a home path and an HTTP status line
|
|
2797
|
+
// are never the rest of a URL, however much URL punctuation they carry.
|
|
2798
|
+
if (/^[\w.-]+@[\w.-]+:|^~\/|^HTTP\/\d/i.test(token)) break;
|
|
2799
|
+
// Nor is a token that is a whole email address.
|
|
2800
|
+
if (/^[\w.+-]+@[\w-]+(?:\.[\w-]+)+$/.test(token)) break;
|
|
2801
|
+
if (!/[%&=/?.\-_]/.test(token) || /^v?\d+(?:\.\d+)*(?:[-+][\w.-]+)?[.,;:!?)]?$/i.test(token)) break;
|
|
2802
|
+
// A label ends with a colon (`Failure/Error:`, `prometheus.io/scrape:`);
|
|
2803
|
+
// the rest of a URL does not.
|
|
2804
|
+
if (token.endsWith(':')) break;
|
|
2805
|
+
// After a `/`, a `name=value` token is the next assignment or log field
|
|
2806
|
+
// (`API_KEY=...`, `level=info`), not the URL: a query starts with `?`.
|
|
2807
|
+
if (urlText.endsWith('/') && /^[A-Za-z_][\w.-]*=/.test(token)) break;
|
|
2808
|
+
// Nor is a measurement or a clock time: `45%` and `45%|####` progress,
|
|
2809
|
+
// `12.3MB/45.0MB`, `12.3ms`, `10:00:00` (`%7B` stays percent-encoding).
|
|
2810
|
+
if (/^\d+(?:\.\d+)?(?:%(?![0-9A-Fa-f]{2})|(?:ms|s|[kKMGT]i?B)(?![\w-]))|^\d{1,2}:\d{2}/.test(token)) break;
|
|
2811
|
+
// Hyphens, underscores and dots also occur in prose. Without stronger
|
|
2812
|
+
// URL punctuation, the token must finish its cell, not introduce a sentence.
|
|
2813
|
+
const rest = nextText.slice(indent + token.length);
|
|
2814
|
+
if (!/[%&=/?]/.test(token)) {
|
|
2815
|
+
// Code (`client.get()`, `expect(x).toBe(y)`) is not a URL piece, and a
|
|
2816
|
+
// word that ends a sentence (`ready.`, `Done.`) is prose. A wiki title
|
|
2817
|
+
// (`Mercury_(planet)`) and a file name before its period
|
|
2818
|
+
// (`install.html.`) still continue the URL.
|
|
2819
|
+
if (/[^_]\(/.test(token)) break;
|
|
2820
|
+
if (/[.,;!]$/.test(token) && !/\.[A-Za-z][A-Za-z0-9]{1,4}[.,;!]+$/.test(token)) break;
|
|
2821
|
+
if (rest.trim() !== '' && !/^ {2,}/.test(rest)) break;
|
|
2822
|
+
}
|
|
2823
|
+
const nextLogical = terminalLogicalLine(buffer, endRow + 1, cols, bound);
|
|
2824
|
+
if (!nextLogical) break;
|
|
2825
|
+
if (trailingColumns) firstRowUrlEnd = last.rawEnd;
|
|
2826
|
+
text = urlText + nextLogical.text.slice(indent).replace(/\s+$/, '');
|
|
2827
|
+
endRow = nextLogical.endRow;
|
|
2828
|
+
extended = true;
|
|
2829
|
+
}
|
|
2830
|
+
return { startRow: start, endRow, extended, firstRowUrlEnd };
|
|
2831
|
+
};
|
|
2832
|
+
|
|
2833
|
+
// Walk up through earlier logical lines: the nearest one whose URL chain
|
|
2834
|
+
// reaches this row owns it.
|
|
2835
|
+
let start = here.startRow;
|
|
2836
|
+
while (start >= 0 && here.startRow - start < bound) {
|
|
2837
|
+
const chain = chainFrom(start);
|
|
2838
|
+
if (chain && chain.extended && chain.endRow >= here.startRow) {
|
|
2839
|
+
return {
|
|
2840
|
+
startRow: chain.startRow,
|
|
2841
|
+
endRow: chain.endRow,
|
|
2842
|
+
...(chain.firstRowUrlEnd === null ? {} : { firstRowUrlEnd: chain.firstRowUrlEnd }),
|
|
2843
|
+
};
|
|
2844
|
+
}
|
|
2845
|
+
if (start === 0) break;
|
|
2846
|
+
const above = terminalLogicalLine(buffer, start - 1, cols, bound);
|
|
2847
|
+
if (!above) break;
|
|
2848
|
+
start = above.startRow;
|
|
2849
|
+
}
|
|
2850
|
+
return null;
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
/**
|
|
2854
|
+
* The text the link provider scans for `row`: {@link terminalPathLine}, widened
|
|
2855
|
+
* to the rows of a URL the CLI wrapped short of the edge
|
|
2856
|
+
* ({@link terminalUrlContinuationExtent}). Same shape as terminalPathLine.
|
|
2857
|
+
*
|
|
2858
|
+
* Over a widened span every row is read trimmed (except one a soft wrap
|
|
2859
|
+
* continues from, which fills the width), a HARD continuation's indent is
|
|
2860
|
+
* dropped (the cell mapping keeps the cells), and the first row stops at the
|
|
2861
|
+
* URL's cell when other table columns follow it, so those columns never sit
|
|
2862
|
+
* between the URL's pieces. Those columns still link: they are moved after the
|
|
2863
|
+
* stitched rows as a segment of their own, mapped back to their cells.
|
|
2864
|
+
*
|
|
2865
|
+
* @returns Same shape as {@link terminalPathLine}; null when the row does not exist.
|
|
2866
|
+
*/
|
|
2867
|
+
function terminalLinkLine(buffer, row, cols, maxRows) {
|
|
2868
|
+
const base = terminalPathLine(buffer, row, cols, maxRows);
|
|
2869
|
+
if (!base) return null;
|
|
2870
|
+
const extent = terminalUrlContinuationExtent(buffer, row, cols, maxRows);
|
|
2871
|
+
if (!extent || (extent.startRow >= base.startRow && extent.endRow <= base.endRow)) return base;
|
|
2872
|
+
const startRow = Math.min(extent.startRow, base.startRow);
|
|
2873
|
+
const endRow = Math.max(extent.endRow, base.endRow);
|
|
2874
|
+
const segments = [];
|
|
2875
|
+
let text = '';
|
|
2876
|
+
let columnsAfterUrl = '';
|
|
2877
|
+
for (let r = startRow; r <= endRow; r++) {
|
|
2878
|
+
const line = buffer.getLine(r);
|
|
2879
|
+
if (!line) break;
|
|
2880
|
+
const next = r < endRow ? buffer.getLine(r + 1) : null;
|
|
2881
|
+
let rowText = line.translateToString(!(next && next.isWrapped)) || '';
|
|
2882
|
+
if (r === extent.startRow && extent.firstRowUrlEnd != null) {
|
|
2883
|
+
columnsAfterUrl = rowText.slice(extent.firstRowUrlEnd);
|
|
2884
|
+
rowText = rowText.slice(0, extent.firstRowUrlEnd);
|
|
2885
|
+
}
|
|
2886
|
+
let colStart = 0;
|
|
2887
|
+
if (r > startRow && !line.isWrapped) {
|
|
2888
|
+
colStart = rowText.length - rowText.replace(/^\s+/, '').length;
|
|
2889
|
+
rowText = rowText.slice(colStart);
|
|
2890
|
+
}
|
|
2891
|
+
segments.push({ row: r, textStart: text.length, colStart });
|
|
2892
|
+
text += rowText;
|
|
2893
|
+
}
|
|
2894
|
+
const lastRow = segments[segments.length - 1].row;
|
|
2895
|
+
// The columns cut off the URL's first row (a path or date column of a table)
|
|
2896
|
+
// are scanned after the stitched rows, so they keep their links on that row.
|
|
2897
|
+
// They open with the cell gap, which keeps them apart from the URL; the one
|
|
2898
|
+
// space before them belongs to the last row, so an end offset there still
|
|
2899
|
+
// maps onto that row and never onto their first cell.
|
|
2900
|
+
if (columnsAfterUrl.trim()) {
|
|
2901
|
+
text += ' ';
|
|
2902
|
+
segments.push({ row: extent.startRow, textStart: text.length, colStart: extent.firstRowUrlEnd });
|
|
2903
|
+
text += columnsAfterUrl;
|
|
2904
|
+
}
|
|
2905
|
+
const offsetToCell = (offset) => {
|
|
2906
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
2907
|
+
const seg = segments[i];
|
|
2908
|
+
if (offset >= seg.textStart || i === 0) return { row: seg.row, col: seg.colStart + (offset - seg.textStart) };
|
|
2909
|
+
}
|
|
2910
|
+
return { row: startRow, col: offset };
|
|
2911
|
+
};
|
|
2912
|
+
// The cut row has two segments: the one starting at or left of the cell owns it.
|
|
2913
|
+
const cellToOffset = (targetRow, targetCol) => {
|
|
2914
|
+
let seg = null;
|
|
2915
|
+
for (const s of segments) if (s.row === targetRow && (!seg || s.colStart <= targetCol)) seg = s;
|
|
2916
|
+
return seg ? seg.textStart + Math.max(0, targetCol - seg.colStart) : -1;
|
|
2917
|
+
};
|
|
2918
|
+
return { startRow, endRow: lastRow, text, offsetToCell, cellToOffset };
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
/**
|
|
2922
|
+
* Absolute file paths the CLI word-wrapped INSIDE a table cell (codex markdown
|
|
2923
|
+
* tables), returned as one link per path with a segment per row.
|
|
2924
|
+
*
|
|
2925
|
+
* Before screenshot (/tmp/x/docs/review/ After screenshot (/tmp/x/review/BLDMESH-
|
|
2926
|
+
* BLDMESH-243/before-overview.png) 243/after-overview.png)
|
|
2927
|
+
*
|
|
2928
|
+
* Neither fragment matches FILE_PATH_LINK_PATTERN alone (the first has no
|
|
2929
|
+
* extension, the second no absolute root), and a linear stitch of the rows
|
|
2930
|
+
* cannot serve a two-column table: each column's continuation sits in the same
|
|
2931
|
+
* next row. So each fragment is followed DOWN ITS OWN CELL: the cell's left
|
|
2932
|
+
* edge is where its text starts after the previous ≥2-space column gap, and the
|
|
2933
|
+
* continuation is the token starting at exactly that column on the next row.
|
|
2934
|
+
* A fragment qualifies only when it is the last thing in its cell on that row
|
|
2935
|
+
* (row end or a ≥2-space gap follows) and ends on `/` or `-`, the only places
|
|
2936
|
+
* a wrapper breaks a path; a cell that ends inside a name (`/home/u/proj` over
|
|
2937
|
+
* `config.yaml`) is a whole value, never the first piece of the next row's. A
|
|
2938
|
+
* continuation row must be blank to the left of the cell (a wrapper wraps every
|
|
2939
|
+
* column of one table row together), unless a path in a left column continued
|
|
2940
|
+
* on it too: a label there (`Entry`, `│ Log │`) is the next record. Every
|
|
2941
|
+
* continuation must pass pathContinuation and its cell must be no wider than the
|
|
2942
|
+
* preceding cell's text, so independent listing rows and longer prose never
|
|
2943
|
+
* displace links.
|
|
2944
|
+
*
|
|
2945
|
+
* Pure: reads rows only. Cells are taken one per string character (xterm rows
|
|
2946
|
+
* with wide glyphs would shift columns; such rows simply fail the alignment).
|
|
2947
|
+
*
|
|
2948
|
+
* @param {{getLine: (row: number) => any, length: number}} buffer
|
|
2949
|
+
* @param {number} row 0-based absolute row being examined.
|
|
2950
|
+
* @param {number} [maxRows] Bound on rows one path may span.
|
|
2951
|
+
* @returns {Array<{path: string, segments: Array<{row: number, startCol: number, endCol: number}>}>}
|
|
2952
|
+
* Paths with a segment on `row`; `endCol` is exclusive, columns 0-based.
|
|
2953
|
+
*/
|
|
2954
|
+
function terminalCellWrappedPaths(buffer, row, maxRows) {
|
|
2955
|
+
const bound = Math.max(2, maxRows || 4);
|
|
2956
|
+
const rootPattern = new RegExp(`\\/(?:${FILE_LINK_ROOT_GROUP})\\/`, 'g');
|
|
2957
|
+
// Box-drawing verticals are cell borders (Claude Code draws tables with them).
|
|
2958
|
+
const isPathChar = (ch) => !!ch && !/[\s"'<>|;&\x00-\x1f│┃║]/.test(ch);
|
|
2959
|
+
const rowText = (r) => {
|
|
2960
|
+
const line = buffer.getLine(r);
|
|
2961
|
+
return line ? (line.translateToString(true) || '').replace(/\s+$/, '') : null;
|
|
2962
|
+
};
|
|
2963
|
+
// A fragment is the last text in its cell on this row when the row ends, a
|
|
2964
|
+
// column gap (≥2 spaces) follows it, or a cell border does.
|
|
2965
|
+
const endsCell = (text, end) => end >= text.length || /^(?: {2,}| *[│┃║|])/.test(text.slice(end));
|
|
2966
|
+
|
|
2967
|
+
const fromStart = (startRow) => {
|
|
2968
|
+
const found = [];
|
|
2969
|
+
const text = rowText(startRow);
|
|
2970
|
+
if (!text) return found;
|
|
2971
|
+
rootPattern.lastIndex = 0;
|
|
2972
|
+
let m;
|
|
2973
|
+
while ((m = rootPattern.exec(text)) !== null) {
|
|
2974
|
+
const start = m.index;
|
|
2975
|
+
if (start > 0 && !/[\s(\[{"'`<]/.test(text[start - 1])) continue;
|
|
2976
|
+
let end = start;
|
|
2977
|
+
while (end < text.length && isPathChar(text[end])) end++;
|
|
2978
|
+
rootPattern.lastIndex = Math.max(end, start + 1);
|
|
2979
|
+
if (!endsCell(text, end)) continue;
|
|
2980
|
+
// Already a complete path on its own row: the ordinary detector owns it.
|
|
2981
|
+
const alone = absoluteFilePathPattern().exec(text.slice(start, end));
|
|
2982
|
+
if (alone && alone.index === 0 && alone[0].length === end - start) continue;
|
|
2983
|
+
// The cell's left edge: just past the nearest ≥2-space gap or cell border
|
|
2984
|
+
// (and the padding after it) before the path.
|
|
2985
|
+
const before = text.slice(0, start);
|
|
2986
|
+
let cellCol = before.length - before.replace(/^\s+/, '').length;
|
|
2987
|
+
const edge = /(?: {2,}|[│┃║|] *)(?=\S)/g;
|
|
2988
|
+
let e;
|
|
2989
|
+
const upToPath = text.slice(0, start + 1);
|
|
2990
|
+
while ((e = edge.exec(upToPath)) !== null) cellCol = e.index + e[0].length;
|
|
2991
|
+
|
|
2992
|
+
let joined = text.slice(start, end);
|
|
2993
|
+
let previousCellLength = end - cellCol;
|
|
2994
|
+
const segments = [{ row: startRow, startCol: start, endCol: end }];
|
|
2995
|
+
for (let r = startRow + 1; r < startRow + bound; r++) {
|
|
2996
|
+
const line = buffer.getLine(r);
|
|
2997
|
+
if (!line || line.isWrapped) break;
|
|
2998
|
+
const next = rowText(r);
|
|
2999
|
+
if (!next || !isPathChar(next[cellCol]) || (cellCol > 0 && next[cellCol - 1] !== ' ')) break;
|
|
3000
|
+
let tokenEnd = cellCol;
|
|
3001
|
+
while (tokenEnd < next.length && isPathChar(next[tokenEnd])) tokenEnd++;
|
|
3002
|
+
const token = next.slice(cellCol, tokenEnd);
|
|
3003
|
+
// Include prose after the token in this cell, but not another column.
|
|
3004
|
+
const cellText = next.slice(cellCol).split(/ {2,}|[│┃║|]/, 1)[0].trimEnd();
|
|
3005
|
+
// A wrapper breaks a path after `/` or `-`; a fragment that ends inside a
|
|
3006
|
+
// name (`/home/u/proj` over `config.yaml`) is a whole value, not a piece.
|
|
3007
|
+
// codex wraps table cells on Unicode line-break opportunities plus a
|
|
3008
|
+
// hyphen splitter, which break after `/` and `-` but never at `_` (UAX #14
|
|
3009
|
+
// class AL).
|
|
3010
|
+
if (!/[/-]$/.test(joined)) break;
|
|
3011
|
+
// A wrapper wraps every column of one table row together, so a row that
|
|
3012
|
+
// carries a cell's continuation is blank to its left unless a path in a
|
|
3013
|
+
// left column continued on that row too. A label there (`Entry`,
|
|
3014
|
+
// `│ Log │`) opens the next record, even under a directory ending in `/`
|
|
3015
|
+
// (`Workspace /home/u/proj/app/` over `Entry main.py`). A left column of
|
|
3016
|
+
// wrapped prose (a `Description | Path` table) fills that row as well, so
|
|
3017
|
+
// the path beside it is not joined and keeps its per-row fragment links:
|
|
3018
|
+
// the safe direction.
|
|
3019
|
+
// Known trade-off: with nothing to the left, a directory written with its
|
|
3020
|
+
// trailing slash over a relative path in the same column (`/home/u/proj/`
|
|
3021
|
+
// over `index.ts`) still joins, since it reads exactly like a wrap at `/`.
|
|
3022
|
+
if (
|
|
3023
|
+
/[^\s│┃║|]/.test(next.slice(0, cellCol)) &&
|
|
3024
|
+
!found.some((hit) => hit.segments.some((seg) => seg.row === r && seg.endCol <= cellCol))
|
|
3025
|
+
) {
|
|
3026
|
+
break;
|
|
3027
|
+
}
|
|
3028
|
+
if (cellText.length > previousCellLength || !pathContinuation(joined, token)) break;
|
|
3029
|
+
const candidate = joined + token;
|
|
3030
|
+
const match = absoluteFilePathPattern().exec(candidate);
|
|
3031
|
+
if (match && match.index === 0 && match[0].length > joined.length) {
|
|
3032
|
+
segments.push({ row: r, startCol: cellCol, endCol: cellCol + match[0].length - joined.length });
|
|
3033
|
+
found.push({ path: match[0], segments });
|
|
3034
|
+
break;
|
|
3035
|
+
}
|
|
3036
|
+
if (!endsCell(next, tokenEnd)) break;
|
|
3037
|
+
joined = candidate;
|
|
3038
|
+
previousCellLength = cellText.length;
|
|
3039
|
+
segments.push({ row: r, startCol: cellCol, endCol: tokenEnd });
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
return found;
|
|
3043
|
+
};
|
|
3044
|
+
|
|
3045
|
+
const results = [];
|
|
3046
|
+
for (let s = row; s >= 0 && row - s < bound; s--) {
|
|
3047
|
+
for (const hit of fromStart(s)) {
|
|
3048
|
+
if (hit.segments.some((seg) => seg.row === row)) results.push(hit);
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
return results;
|
|
3052
|
+
}
|
|
3053
|
+
|
|
2522
3054
|
// ═══════════════════════════════════════════════════════════════
|
|
2523
3055
|
// Split-Pane Sessions — pure helpers (divider math, picker list)
|
|
2524
3056
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -3349,7 +3881,9 @@ function clampTerminalDimensions(proposed) {
|
|
|
3349
3881
|
* could reach it. Output visible, typing invisible, for as long as the claim
|
|
3350
3882
|
* stayed hot. Width is the axis the wrap arithmetic depends on; rows only
|
|
3351
3883
|
* decide how much is on screen at once, and keeping the local row count keeps
|
|
3352
|
-
* the composer at the bottom of a viewport that scrolls.
|
|
3884
|
+
* the composer at the bottom of a viewport that scrolls. The one deliberate
|
|
3885
|
+
* screen taller than its box is this device's own rows held under the
|
|
3886
|
+
* on-screen keyboard, which keyboardClipPx keeps the composer in view for.
|
|
3353
3887
|
*
|
|
3354
3888
|
* @param {{cols: number, rows: number}|null} local - what xterm currently holds
|
|
3355
3889
|
* @param {{cols: number, rows: number}|null} pty - what the server just reported
|
|
@@ -3361,6 +3895,62 @@ function reconcilePtyGeometry(local, pty) {
|
|
|
3361
3895
|
return { adopt: true, cols: pty.cols };
|
|
3362
3896
|
}
|
|
3363
3897
|
|
|
3898
|
+
/** Rows below the last content within which the cursor still counts (keyboardClipPx). */
|
|
3899
|
+
const KEYBOARD_CLIP_CURSOR_REACH = 2;
|
|
3900
|
+
|
|
3901
|
+
/**
|
|
3902
|
+
* How far to scroll the terminal SCREEN up while its rows are held under the
|
|
3903
|
+
* on-screen keyboard (setting `keyboardHoldsRows`).
|
|
3904
|
+
*
|
|
3905
|
+
* Every PTY height change makes Claude Code clear the screen and reprint only
|
|
3906
|
+
* the transcript tail that fits, so rows above that tail are lost from tmux and
|
|
3907
|
+
* older ones stay in history as stale copies. The keyboard opening and closing
|
|
3908
|
+
* was such a change each time. Instead the rows stay as they were and
|
|
3909
|
+
* `.xterm-screen` (taller than its box now) is translated up by this many px.
|
|
3910
|
+
*
|
|
3911
|
+
* This is the one deliberate exception to "a screen taller than its container
|
|
3912
|
+
* hides the input line" (see reconcilePtyGeometry): the offset below is what
|
|
3913
|
+
* keeps the live content in view.
|
|
3914
|
+
*
|
|
3915
|
+
* - Following live output: the last row with content sits at the bottom of the
|
|
3916
|
+
* box. A fresh shell prompting on row 1 shows the top; Claude's footer under
|
|
3917
|
+
* its composer stays visible. The cursor counts only when it is right below
|
|
3918
|
+
* that content (a prompt or composer line still empty): a TUI that parks its
|
|
3919
|
+
* cursor far below what it drew (codex's dialogs, with the cursor on the last
|
|
3920
|
+
* row of an otherwise blank screen) must not scroll its content out of view.
|
|
3921
|
+
* - Reading history: the offset follows the scroll position, so the top of the
|
|
3922
|
+
* scrollback is reachable and scrolling stays continuous.
|
|
3923
|
+
*
|
|
3924
|
+
* Recomputed on every render with no hysteresis; add some if a TUI's mid-redraw
|
|
3925
|
+
* frames ever make the view jitter.
|
|
3926
|
+
*
|
|
3927
|
+
* @param {{rows: number, cellHeight: number, viewHeight: number, cursorRow: number,
|
|
3928
|
+
* lastContentRow: number, following: boolean, viewportY: number, baseY: number}} input
|
|
3929
|
+
* lastContentRow: the last non-blank screen row, or -1 when the screen is blank
|
|
3930
|
+
* @returns {number} px, between 0 and the screen's overflow
|
|
3931
|
+
*/
|
|
3932
|
+
function keyboardClipPx(input) {
|
|
3933
|
+
const { rows, cellHeight, viewHeight, cursorRow, lastContentRow, following, viewportY, baseY } = input;
|
|
3934
|
+
if (!(cellHeight > 0) || !(viewHeight > 0) || !(rows > 0)) return 0;
|
|
3935
|
+
const max = Math.max(0, rows * cellHeight - viewHeight);
|
|
3936
|
+
if (max === 0) return 0;
|
|
3937
|
+
if (!following && baseY > 0) return Math.round(max * Math.min(1, Math.max(0, viewportY / baseY)));
|
|
3938
|
+
const cursorCounts = lastContentRow < 0 || cursorRow <= lastContentRow + KEYBOARD_CLIP_CURSOR_REACH;
|
|
3939
|
+
const contentBottomRow = cursorCounts ? Math.max(cursorRow, lastContentRow) : lastContentRow;
|
|
3940
|
+
const bottom = (Math.min(rows - 1, Math.max(0, contentBottomRow)) + 1) * cellHeight;
|
|
3941
|
+
return Math.round(Math.min(max, Math.max(0, bottom - viewHeight)));
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3944
|
+
/**
|
|
3945
|
+
* How many screen rows, from the top, are fully visible at a given offset: the
|
|
3946
|
+
* local-echo overlay grows upward from the last of them (`setRowLimit`).
|
|
3947
|
+
* @returns {number}
|
|
3948
|
+
*/
|
|
3949
|
+
function keyboardVisibleRowLimit(clipPx, viewHeight, cellHeight) {
|
|
3950
|
+
if (!(cellHeight > 0)) return 0;
|
|
3951
|
+
return Math.max(1, Math.floor((clipPx + viewHeight) / cellHeight));
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3364
3954
|
/**
|
|
3365
3955
|
* Which session does a dashboard URL's fragment ask for? Another page that
|
|
3366
3956
|
* holds the dashboard's window, such as a task board, points it at
|
|
@@ -3440,7 +4030,14 @@ if (typeof window !== 'undefined') {
|
|
|
3440
4030
|
rendersAsPage,
|
|
3441
4031
|
HTML_PAGE_EXTENSIONS,
|
|
3442
4032
|
};
|
|
3443
|
-
window.CodemanTerminalLines = {
|
|
4033
|
+
window.CodemanTerminalLines = {
|
|
4034
|
+
terminalLogicalLine,
|
|
4035
|
+
terminalPathLine,
|
|
4036
|
+
terminalLinkLine,
|
|
4037
|
+
terminalUrlContinuationExtent,
|
|
4038
|
+
findTerminalUrls,
|
|
4039
|
+
terminalCellWrappedPaths,
|
|
4040
|
+
};
|
|
3444
4041
|
window.CodemanUrlSession = { sessionIdFromFragment };
|
|
3445
4042
|
window.CodemanSplitPane = {
|
|
3446
4043
|
clampDividerPercent,
|
|
@@ -3491,6 +4088,8 @@ if (typeof window !== 'undefined') {
|
|
|
3491
4088
|
window.CodemanTerminalGeometry = {
|
|
3492
4089
|
clampTerminalDimensions,
|
|
3493
4090
|
reconcilePtyGeometry,
|
|
4091
|
+
keyboardClipPx,
|
|
4092
|
+
keyboardVisibleRowLimit,
|
|
3494
4093
|
TERMINAL_MIN_COLS,
|
|
3495
4094
|
TERMINAL_MIN_ROWS,
|
|
3496
4095
|
};
|