@phenx-inc/ctlsurf 0.17.2 → 0.17.3
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 +25 -2
- package/out/headless/index.mjs +1033 -128
- package/out/headless/index.mjs.map +4 -4
- package/out/main/index.js +929 -17
- package/out/preload/index.js +15 -0
- package/out/renderer/assets/{cssMode-BtQxI2Eb.js → cssMode-BJfoToLX.js} +3 -3
- package/out/renderer/assets/{freemarker2-DRW5dmY5.js → freemarker2-T0eMtP1Z.js} +1 -1
- package/out/renderer/assets/{handlebars-CFLF3cP7.js → handlebars-Dbbib8fi.js} +1 -1
- package/out/renderer/assets/{html-DtnBOkwp.js → html-BRHT_oxc.js} +1 -1
- package/out/renderer/assets/{htmlMode-DMVysDCg.js → htmlMode-BRlZTxME.js} +3 -3
- package/out/renderer/assets/{index-CEjwKjtd.css → index-CBWAeUYW.css} +91 -0
- package/out/renderer/assets/{index-Dm70hk5s.js → index-RCAeuJTr.js} +476 -411
- package/out/renderer/assets/{javascript-ChlyA1sf.js → javascript-BbOZOxq9.js} +2 -2
- package/out/renderer/assets/{jsonMode-D1_oHzeN.js → jsonMode-CUvAeYcT.js} +3 -3
- package/out/renderer/assets/{liquid-D4ftdy1G.js → liquid-DRIACo_s.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-Ckqu5ubG.js → lspLanguageFeatures-Ds-3nH9p.js} +1 -1
- package/out/renderer/assets/{mdx-B9wsVYSC.js → mdx-D76jlpLO.js} +1 -1
- package/out/renderer/assets/{python-e23tD6IS.js → python-CMuRPKJi.js} +1 -1
- package/out/renderer/assets/{razor-CrDyXj3d.js → razor-DRoH5mal.js} +1 -1
- package/out/renderer/assets/{tsMode-BwGC5o4O.js → tsMode-dPJzgFD7.js} +1 -1
- package/out/renderer/assets/{typescript-Do-98d_O.js → typescript-D5USKnLj.js} +1 -1
- package/out/renderer/assets/{xml-Dpdu5R3m.js → xml-qYg1wt2P.js} +1 -1
- package/out/renderer/assets/{yaml-B0Zt7LCJ.js → yaml-Uc-CvnvL.js} +1 -1
- package/out/renderer/index.html +2 -2
- package/package.json +4 -2
- package/scripts/check-work-map-gpu.cjs +62 -0
- package/scripts/check-work-map-gpu.d.cts +13 -0
- package/scripts/postinstall.cjs +15 -0
- package/scripts/prepare-work-map.py +37 -0
- package/scripts/test-work-map-capability.cjs +45 -0
- package/scripts/test-work-map-ui.cjs +130 -0
- package/scripts/test-work-map.cjs +352 -0
- package/scripts/work-map-clef.py +81 -0
- package/src/main/index.ts +35 -1
- package/src/main/observer/observer.ts +2 -0
- package/src/main/orchestrator.ts +39 -4
- package/src/main/transcripts.ts +35 -16
- package/src/main/workMap/clef.ts +156 -0
- package/src/main/workMap/normalize.ts +172 -0
- package/src/main/workMap/source.ts +28 -0
- package/src/main/workMap/tree.ts +199 -0
- package/src/main/workMap/workMap.ts +244 -0
- package/src/preload/index.ts +17 -0
- package/src/renderer/App.tsx +15 -16
- package/src/renderer/components/ObserverPanel.tsx +3 -0
- package/src/renderer/components/SettingsDialog.tsx +0 -104
- package/src/renderer/components/WorkMapPanel.tsx +235 -0
- package/src/renderer/components/WorkMapToggle.tsx +50 -0
- package/src/renderer/lib/workMapHighlights.ts +41 -0
- package/src/renderer/styles.css +91 -0
- package/src/shared/workMap.d.ts +70 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { conf as conf$1, language as language$1 } from "./typescript-
|
|
2
|
-
import "./index-
|
|
1
|
+
import { conf as conf$1, language as language$1 } from "./typescript-D5USKnLj.js";
|
|
2
|
+
import "./index-RCAeuJTr.js";
|
|
3
3
|
const conf = conf$1;
|
|
4
4
|
const language = {
|
|
5
5
|
// Set defaultToken to invalid to see what you do not tokenize yet
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { c as createWebWorker, l as languages, e as editor } from "./index-
|
|
2
|
-
import { f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter, H as HoverAdapter, b as DocumentSymbolAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, S as SelectionRangeAdapter, e as DiagnosticsAdapter } from "./lspLanguageFeatures-
|
|
3
|
-
import { a, D, h, R, c, i, j, t, k } from "./lspLanguageFeatures-
|
|
1
|
+
import { c as createWebWorker, l as languages, e as editor } from "./index-RCAeuJTr.js";
|
|
2
|
+
import { f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter, H as HoverAdapter, b as DocumentSymbolAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, S as SelectionRangeAdapter, e as DiagnosticsAdapter } from "./lspLanguageFeatures-Ds-3nH9p.js";
|
|
3
|
+
import { a, D, h, R, c, i, j, t, k } from "./lspLanguageFeatures-Ds-3nH9p.js";
|
|
4
4
|
const STOP_WHEN_IDLE_FOR = 2 * 60 * 1e3;
|
|
5
5
|
class WorkerManager {
|
|
6
6
|
constructor(defaults) {
|
package/out/renderer/assets/{lspLanguageFeatures-Ckqu5ubG.js → lspLanguageFeatures-Ds-3nH9p.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { R as Range$1, l as languages, e as editor, U as Uri, M as MarkerSeverity } from "./index-
|
|
1
|
+
import { R as Range$1, l as languages, e as editor, U as Uri, M as MarkerSeverity } from "./index-RCAeuJTr.js";
|
|
2
2
|
var DocumentUri;
|
|
3
3
|
(function(DocumentUri2) {
|
|
4
4
|
function is(value) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as createWebWorker, e as editor, U as Uri, a as MarkerTag, M as MarkerSeverity, l as languages, t as typescriptDefaults, R as Range } from "./index-
|
|
1
|
+
import { c as createWebWorker, e as editor, U as Uri, a as MarkerTag, M as MarkerSeverity, l as languages, t as typescriptDefaults, R as Range } from "./index-RCAeuJTr.js";
|
|
2
2
|
class WorkerManager {
|
|
3
3
|
constructor(_modeId, _defaults) {
|
|
4
4
|
this._modeId = _modeId;
|
package/out/renderer/index.html
CHANGED
|
@@ -106,8 +106,8 @@
|
|
|
106
106
|
:root[data-theme='catppuccin-latte'] .boot-splash-title { color: #2f3145; }
|
|
107
107
|
:root[data-theme='catppuccin-latte'] .boot-splash-label { color: #7c7f93; }
|
|
108
108
|
</style>
|
|
109
|
-
<script type="module" crossorigin src="./assets/index-
|
|
110
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
109
|
+
<script type="module" crossorigin src="./assets/index-RCAeuJTr.js"></script>
|
|
110
|
+
<link rel="stylesheet" crossorigin href="./assets/index-CBWAeUYW.css">
|
|
111
111
|
</head>
|
|
112
112
|
<body>
|
|
113
113
|
<div id="boot-splash">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phenx-inc/ctlsurf",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.3",
|
|
4
4
|
"description": "Agent-agnostic terminal and desktop app for ctlsurf — run Claude Code, Codex, or any coding agent with live session logging and remote control",
|
|
5
5
|
"main": "out/main/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
"THIRD-PARTY-NOTICES.md"
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
|
+
"test:workmap": "node scripts/test-work-map-capability.cjs && node scripts/test-work-map.cjs",
|
|
23
|
+
"test:workmap:ui": "electron scripts/test-work-map-ui.cjs",
|
|
22
24
|
"dev": "electron-vite dev",
|
|
23
25
|
"dev:terminal": "esbuild src/main/headless.ts --bundle --platform=node --target=node18 --format=esm --outfile=out/headless/index.mjs --external:node-pty --external:ws --sourcemap && node out/headless/index.mjs",
|
|
24
26
|
"build": "electron-vite build && npm run build:headless",
|
|
@@ -26,7 +28,7 @@
|
|
|
26
28
|
"prepublishOnly": "npm run build",
|
|
27
29
|
"preview": "electron-vite preview",
|
|
28
30
|
"package": "electron-builder",
|
|
29
|
-
"postinstall": "node
|
|
31
|
+
"postinstall": "node scripts/postinstall.cjs"
|
|
30
32
|
},
|
|
31
33
|
"keywords": [
|
|
32
34
|
"ctlsurf",
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Shared by npm postinstall and the desktop startup check. No model/runtime downloads.
|
|
2
|
+
|
|
3
|
+
function initialCapability({ desktop = false, platform = process.platform, arch = process.arch } = {}) {
|
|
4
|
+
if (!desktop) return { state: 'unavailable', reason: 'Work map is available only in the desktop app.' }
|
|
5
|
+
if (platform !== 'darwin' || arch !== 'arm64') return { state: 'unavailable', reason: 'Work map currently requires an Apple Silicon Mac with Metal GPU support.' }
|
|
6
|
+
return { state: 'checking', reason: 'Checking Apple GPU availability…' }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function appleGpuFromReport(report) {
|
|
10
|
+
if (!Array.isArray(report?.SPDisplaysDataType)) return null
|
|
11
|
+
return report.SPDisplaysDataType.find(gpu => {
|
|
12
|
+
const apple = gpu.spdisplays_vendor === 'sppci_vendor_Apple' || /^Apple M\d/i.test(gpu.sppci_model || gpu._name || '')
|
|
13
|
+
const metal = String(gpu.spdisplays_metal || gpu.spdisplays_metalsupport || gpu.spdisplays_mtlgpufamilysupport || '')
|
|
14
|
+
return apple && /supported|metal/i.test(metal) && !/unsupported|not.supported|unavailable/i.test(metal)
|
|
15
|
+
}) || null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function usableElectronGpu(info) {
|
|
19
|
+
if (info?.auxAttributes?.softwareRendering === true) return false
|
|
20
|
+
return Array.isArray(info?.gpuDevice) && info.gpuDevice.some(gpu => gpu.active === true
|
|
21
|
+
&& (gpu.vendorId === 0x106b || /Apple/i.test(gpu.deviceString || gpu.vendorString || '')))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readGpuReport() {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const { execFile } = require('node:child_process')
|
|
27
|
+
execFile('/usr/sbin/system_profiler', ['SPDisplaysDataType', '-json'], { timeout: 5000, maxBuffer: 1024 * 1024 }, (error, output) => {
|
|
28
|
+
if (error) { reject(error); return }
|
|
29
|
+
try { resolve(JSON.parse(output)) } catch (error) { reject(error) }
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function detectCapability(options = {}) {
|
|
35
|
+
const initial = initialCapability(options)
|
|
36
|
+
if (initial.state !== 'checking') return initial
|
|
37
|
+
try {
|
|
38
|
+
const gpu = appleGpuFromReport(await (options.readGpuReport || readGpuReport)())
|
|
39
|
+
if (!gpu) return { state: 'unavailable', reason: 'No Apple GPU with Metal support was detected.' }
|
|
40
|
+
if (options.verifyGpu) {
|
|
41
|
+
let timer
|
|
42
|
+
const usable = await Promise.race([
|
|
43
|
+
options.verifyGpu(),
|
|
44
|
+
new Promise(resolve => { timer = setTimeout(() => resolve(false), 5000) }),
|
|
45
|
+
]).finally(() => clearTimeout(timer))
|
|
46
|
+
if (!usable) return { state: 'unavailable', reason: 'The Apple GPU is unavailable to the desktop app.' }
|
|
47
|
+
}
|
|
48
|
+
return { state: 'available', reason: null, gpuName: String(gpu.sppci_model || gpu._name || 'Apple GPU') }
|
|
49
|
+
} catch {
|
|
50
|
+
return { state: 'unavailable', reason: 'Apple GPU availability could not be verified. Work map is disabled for this launch.' }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { initialCapability, appleGpuFromReport, usableElectronGpu, detectCapability }
|
|
55
|
+
|
|
56
|
+
if (require.main === module) {
|
|
57
|
+
detectCapability({ desktop: true }).then(capability => {
|
|
58
|
+
console.log(capability.state === 'available'
|
|
59
|
+
? `[ctlsurf] Work map hardware detected: ${capability.gpuName}. The desktop app checks GPU access again at startup.`
|
|
60
|
+
: `[ctlsurf] ${capability.reason} Other app features remain available.`)
|
|
61
|
+
}).catch(() => {}) // Optional feature detection never fails package installation.
|
|
62
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { MapCapability } from '../src/shared/workMap'
|
|
2
|
+
|
|
3
|
+
interface CapabilityOptions {
|
|
4
|
+
desktop?: boolean
|
|
5
|
+
platform?: string
|
|
6
|
+
arch?: string
|
|
7
|
+
readGpuReport?: () => Promise<unknown>
|
|
8
|
+
verifyGpu?: () => Promise<boolean>
|
|
9
|
+
}
|
|
10
|
+
export function initialCapability(options?: CapabilityOptions): MapCapability
|
|
11
|
+
export function appleGpuFromReport(report: unknown): Record<string, unknown> | null
|
|
12
|
+
export function usableElectronGpu(info: unknown): boolean
|
|
13
|
+
export function detectCapability(options?: CapabilityOptions): Promise<MapCapability>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const { spawnSync } = require('node:child_process')
|
|
2
|
+
const { detectCapability } = require('./check-work-map-gpu.cjs')
|
|
3
|
+
|
|
4
|
+
// Keep the existing best-effort native rebuild, including on Windows.
|
|
5
|
+
try {
|
|
6
|
+
spawnSync(process.platform === 'win32' ? 'node-gyp.cmd' : 'node-gyp', ['rebuild', '--directory=node_modules/node-pty'], {
|
|
7
|
+
stdio: 'ignore', shell: process.platform === 'win32',
|
|
8
|
+
})
|
|
9
|
+
} catch { /* The launcher also handles missing native builds. */ }
|
|
10
|
+
|
|
11
|
+
detectCapability({ desktop: true }).then(capability => {
|
|
12
|
+
console.log(capability.state === 'available'
|
|
13
|
+
? `[ctlsurf] Work map hardware detected: ${capability.gpuName}. GPU access is checked again by the desktop app.`
|
|
14
|
+
: `[ctlsurf] ${capability.reason} Other app features remain available.`)
|
|
15
|
+
}).catch(() => {})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Prepare only the local Clef-Flash engine. Requires uv and Python 3.12 on Apple Silicon."""
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
13
|
+
parser.add_argument('--directory', type=Path, default=Path.home() / '.ctlsurf' / 'clef-flash')
|
|
14
|
+
args = parser.parse_args()
|
|
15
|
+
if sys.platform != 'darwin' or platform.machine() != 'arm64':
|
|
16
|
+
parser.error('Local Clef-Flash requires an Apple Silicon Mac')
|
|
17
|
+
root = args.directory.resolve()
|
|
18
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
python = root / 'env' / 'bin' / 'python'
|
|
20
|
+
model = root / 'model'
|
|
21
|
+
env = {**os.environ, 'UV_CACHE_DIR': str(root / 'uv'), 'HF_HOME': str(root / 'huggingface'),
|
|
22
|
+
'HF_HUB_DISABLE_TELEMETRY': '1', 'TOKENIZERS_PARALLELISM': 'false'}
|
|
23
|
+
if not python.exists():
|
|
24
|
+
subprocess.run(['uv', 'venv', '--python', '3.12', str(root / 'env')], env=env, check=True)
|
|
25
|
+
subprocess.run(['uv', 'pip', 'install', '--python', str(python), 'huggingface_hub',
|
|
26
|
+
'mlx==0.32.3', 'mlx-lm==0.32.0', 'mlx-vlm==0.7.4'], env=env, check=True)
|
|
27
|
+
subprocess.run([str(python), '-c',
|
|
28
|
+
'from huggingface_hub import snapshot_download; import sys; '
|
|
29
|
+
'snapshot_download("mlx-community/clef-flash-8bit", '
|
|
30
|
+
'revision="5a195dc147926b63b1d139691ed4480ca9f92c0e", local_dir=sys.argv[1], '
|
|
31
|
+
'ignore_patterns=["*.png","*.jpg","*.parquet","*.csv","*.ipynb"])', str(model)], env=env, check=True)
|
|
32
|
+
print('\nUse these paths in Work map → Local model setup:')
|
|
33
|
+
print(json.dumps({'pythonPath': str(python), 'modelPath': str(model)}, indent=2))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == '__main__':
|
|
37
|
+
main()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const assert = require('node:assert/strict')
|
|
2
|
+
const { initialCapability, appleGpuFromReport, usableElectronGpu, detectCapability } = require('./check-work-map-gpu.cjs')
|
|
3
|
+
|
|
4
|
+
async function main() {
|
|
5
|
+
const report = { SPDisplaysDataType: [{ _name: 'Apple M5 Max', spdisplays_vendor: 'sppci_vendor_Apple', spdisplays_metal: 'spdisplays_supported' }] }
|
|
6
|
+
assert(appleGpuFromReport(report))
|
|
7
|
+
assert(appleGpuFromReport({ SPDisplaysDataType: [{ _name: 'Apple M5 Max', spdisplays_mtlgpufamilysupport: 'spdisplays_metal4' }] }))
|
|
8
|
+
assert.equal(appleGpuFromReport({ SPDisplaysDataType: [{ ...report.SPDisplaysDataType[0], spdisplays_metal: 'spdisplays_unsupported' }] }), null)
|
|
9
|
+
assert.equal(appleGpuFromReport({ SPDisplaysDataType: [{ _name: 'Intel GPU', spdisplays_metal: 'spdisplays_supported' }] }), null)
|
|
10
|
+
assert.equal(appleGpuFromReport(null), null)
|
|
11
|
+
assert.equal(usableElectronGpu({ gpuDevice: [{ active: true, vendorId: 0x106b }] }), true)
|
|
12
|
+
assert.equal(usableElectronGpu({ gpuDevice: [{ active: true, vendorId: 0x106b }], auxAttributes: { softwareRendering: true } }), false)
|
|
13
|
+
assert.equal(usableElectronGpu({ gpuDevice: [{ active: false, vendorId: 0x106b }] }), false)
|
|
14
|
+
|
|
15
|
+
let probes = 0
|
|
16
|
+
const readGpuReport = async () => { probes++; return report }
|
|
17
|
+
for (const options of [
|
|
18
|
+
{ desktop: false, platform: 'darwin', arch: 'arm64' },
|
|
19
|
+
{ desktop: true, platform: 'darwin', arch: 'x64' },
|
|
20
|
+
{ desktop: true, platform: 'win32', arch: 'arm64' },
|
|
21
|
+
{ desktop: true, platform: 'linux', arch: 'x64' },
|
|
22
|
+
]) {
|
|
23
|
+
assert.equal(initialCapability(options).state, 'unavailable')
|
|
24
|
+
assert.equal((await detectCapability({ ...options, readGpuReport })).state, 'unavailable')
|
|
25
|
+
}
|
|
26
|
+
assert.equal(probes, 0, 'terminal and unsupported platforms never launch a GPU probe')
|
|
27
|
+
const options = { desktop: true, platform: 'darwin', arch: 'arm64', readGpuReport }
|
|
28
|
+
assert.equal(initialCapability(options).state, 'checking')
|
|
29
|
+
assert.equal((await detectCapability({ ...options, verifyGpu: async () => true })).state, 'available')
|
|
30
|
+
assert.equal((await detectCapability({ ...options, verifyGpu: async () => false })).state, 'unavailable')
|
|
31
|
+
assert.equal((await detectCapability({ ...options, readGpuReport: async () => { throw new Error('probe failed') } })).state, 'unavailable')
|
|
32
|
+
// The headless output is ESM: importing shared detection must not execute CJS-only GPU code.
|
|
33
|
+
const fs = require('node:fs/promises')
|
|
34
|
+
const path = require('node:path')
|
|
35
|
+
const root = await fs.mkdtemp(path.join(require('node:os').tmpdir(), 'ctlsurf-gpu-esm-'))
|
|
36
|
+
try {
|
|
37
|
+
const file = path.join(root, 'probe.mjs')
|
|
38
|
+
await require('esbuild').build({ stdin: { contents: "export { initialCapability } from './scripts/check-work-map-gpu.cjs'", resolveDir: path.resolve(__dirname, '..') },
|
|
39
|
+
bundle: true, platform: 'node', format: 'esm', outfile: file })
|
|
40
|
+
const probe = await import(require('node:url').pathToFileURL(file).href)
|
|
41
|
+
assert.equal(probe.initialCapability({ desktop: false }).state, 'unavailable')
|
|
42
|
+
} finally { await fs.rm(root, { recursive: true, force: true }) }
|
|
43
|
+
console.log('PASS Apple Metal detection, desktop GPU access, software rendering, platform and terminal exclusions')
|
|
44
|
+
}
|
|
45
|
+
main().catch(error => { console.error(error); process.exitCode = 1 })
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Render the real panel in an isolated Electron window, with a deterministic IPC fixture.
|
|
2
|
+
const { app, BrowserWindow } = require('electron')
|
|
3
|
+
const { build } = require('esbuild')
|
|
4
|
+
const fs = require('node:fs/promises')
|
|
5
|
+
const path = require('node:path')
|
|
6
|
+
const os = require('node:os')
|
|
7
|
+
const assert = require('node:assert/strict')
|
|
8
|
+
app.on('window-all-closed', () => {})
|
|
9
|
+
|
|
10
|
+
async function main() {
|
|
11
|
+
const repo = path.resolve(__dirname, '..')
|
|
12
|
+
const temp = await fs.mkdtemp(path.join(os.tmpdir(), 'ctlsurf-map-ui-'))
|
|
13
|
+
app.setPath('userData', path.join(temp, 'profile'))
|
|
14
|
+
let win
|
|
15
|
+
try {
|
|
16
|
+
await build({ stdin: { resolveDir: repo, loader: 'tsx', contents: String.raw`
|
|
17
|
+
import React from 'react'; import {createRoot} from 'react-dom/client';
|
|
18
|
+
import {WorkMapPanel} from './src/renderer/components/WorkMapPanel';
|
|
19
|
+
import {WorkMapToggle} from './src/renderer/components/WorkMapToggle';
|
|
20
|
+
import './src/renderer/styles.css';
|
|
21
|
+
const ts = new Date().toISOString();
|
|
22
|
+
const evidence = (i) => [{id:'event-'+i,ts,kind:'tool',tool:'shell',toolId:'call-'+i,source:'rollout-example.jsonl',text:'npm run test -- terminal-scroll\nProcess exited with code 1'}];
|
|
23
|
+
const nodes = [
|
|
24
|
+
{id:'goal',parentId:null,kind:'goal',title:'Keep the terminal steady while reading older output',activity:'planning',status:'unknown',ts,evidence:[]},
|
|
25
|
+
{id:'branch',parentId:'goal',kind:'branch',title:'Terminal scrolling',activity:'planning',status:'unknown',ts,evidence:[]},
|
|
26
|
+
{id:'thought',parentId:'branch',kind:'step',title:'Preserve the reader’s position instead of forcing the viewport down.',focus:{text:'Preserve the reader’s position instead of forcing the viewport down',reason:'so new output does not interrupt reading older activity.'},activity:'thinking',intent:'decision',status:'unknown',ts,evidence:[{id:'thought-event',ts,kind:'thinking',source:'rollout-example.jsonl',text:'Preserve the reader’s position instead of forcing the viewport down, so new output does not interrupt reading older activity.'}],placement:'clef-flash',confidence:.93},
|
|
27
|
+
...Array.from({length:28},(_,i)=>({id:'step-'+i,parentId:'branch',contextId:'thought',kind:'step',title:['Inspect the viewport and scroll listeners','Preserve the reader’s position during new output','Check terminal scroll regression'][i%3],activity:['investigating','editing','testing'][i%3],status:i===2?'failed':i===27?'running':'succeeded',ts,evidence:evidence(i),placement:'clef-flash',confidence:.93}))
|
|
28
|
+
];
|
|
29
|
+
window.exampleNodes = nodes;
|
|
30
|
+
let capability={state:'available',reason:null,gpuName:'Test GPU'};
|
|
31
|
+
let capabilityListener;
|
|
32
|
+
window.emitCapability = (next) => {capability=next; capabilityListener?.(next);window.emitMap({capability:next,settings:{...snapshot.settings,enabled:next.state==='available'}})};
|
|
33
|
+
let snapshot = {revision:1,tabId:'test',agentName:'Codex',supported:true,ended:false,state:'busy',settings:{enabled:true,engine:'clef-flash',pythonPath:'/local/python',modelPath:'/local/clef-flash'},runtime:'ready',error:null,nodes,dropped:0,thinkingStatus:'available',capability};
|
|
34
|
+
let listener;
|
|
35
|
+
window.emitMap = (update) => { snapshot={...snapshot,...update,revision:snapshot.revision+1}; listener?.(snapshot); };
|
|
36
|
+
window.mapSnapshot = () => snapshot;
|
|
37
|
+
window.worker = {
|
|
38
|
+
getWorkMap:async()=>snapshot,
|
|
39
|
+
getWorkMapCapability:async()=>capability,
|
|
40
|
+
onWorkMapCapability:callback=>{capabilityListener=callback;return()=>{capabilityListener=null}},
|
|
41
|
+
getWorkMapSource:async(_,eventId)=>({source:'rollout-example.jsonl',text:'Earlier context before the choice.\n\nPreserve the reader’s position instead of forcing the viewport down, so new output does not interrupt reading older activity.\n\nLater context after the choice, which is not highlighted.'}),
|
|
42
|
+
setWorkMap:async(_,settings)=>{window.emitMap({settings:{...snapshot.settings,...settings}});return snapshot},
|
|
43
|
+
clearWorkMap:async()=>{window.emitMap({nodes:[]});return snapshot},
|
|
44
|
+
onWorkMapUpdate:callback=>{listener=callback;return()=>{listener=null}},
|
|
45
|
+
};
|
|
46
|
+
createRoot(document.getElementById('root')).render(<><WorkMapToggle visible onToggle={()=>{}} onUnavailable={()=>{}} /><div style={{flex:1,minHeight:0}}><WorkMapPanel tabId="test" /></div></>);
|
|
47
|
+
` }, bundle: true, platform: 'browser', jsx: 'automatic', outfile: path.join(temp, 'panel.js') })
|
|
48
|
+
await fs.writeFile(path.join(temp, 'index.html'), `<!doctype html><html data-theme="tokyo-night"><head><meta charset="utf-8"><link rel="stylesheet" href="panel.css"><style>html,body,#root{height:100%;width:100%;margin:0;overflow:hidden}#root{display:flex;flex-direction:column}body{font-family:-apple-system,BlinkMacSystemFont,sans-serif}</style></head><body><div id="root"></div><script src="panel.js"></script></body></html>`)
|
|
49
|
+
await app.whenReady()
|
|
50
|
+
if (process.platform === 'darwin' && process.arch === 'arm64') {
|
|
51
|
+
const { detectCapability, usableElectronGpu } = require('./check-work-map-gpu.cjs')
|
|
52
|
+
const actual = await detectCapability({ desktop: true, verifyGpu: async () => usableElectronGpu(await app.getGPUInfo('complete')) })
|
|
53
|
+
assert.equal(actual.state, 'available', JSON.stringify(actual))
|
|
54
|
+
console.log('PASS actual desktop Apple GPU access:', actual.gpuName)
|
|
55
|
+
}
|
|
56
|
+
win = new BrowserWindow({ show: false, width: 430, height: 860, webPreferences: { backgroundThrottling: false } })
|
|
57
|
+
await win.loadFile(path.join(temp, 'index.html'))
|
|
58
|
+
const run = code => win.webContents.executeJavaScript(code)
|
|
59
|
+
await run(`new Promise(resolve=>setTimeout(resolve,150))`)
|
|
60
|
+
assert.equal(await run(`document.querySelectorAll('.work-map-step').length`), 29, 'choices and actions must share one thread')
|
|
61
|
+
assert.equal(await run(`document.querySelector('.work-map-view')`), null, 'there are no separate decision/activity tabs')
|
|
62
|
+
assert.match(await run(`document.querySelector('.work-map-reason').textContent`), /Why.*does not interrupt/)
|
|
63
|
+
assert.equal(await run(`document.querySelectorAll('.work-map-step.follows-intent').length`), 28)
|
|
64
|
+
const order = await run(`[...document.querySelectorAll('.work-map-step-title')].map(e=>e.textContent)`)
|
|
65
|
+
await run(`window.emitMap({nodes:[...window.mapSnapshot().nodes.map(n=>n.id==='step-1'?{...n,parentId:'other-topic'}:n),{id:'other-topic',kind:'branch',parentId:'goal',title:'Another topic',activity:'planning',status:'unknown',ts:new Date().toISOString(),evidence:[]}]})`)
|
|
66
|
+
await run(`new Promise(resolve=>setTimeout(resolve,30))`)
|
|
67
|
+
assert.deepEqual(await run(`[...document.querySelectorAll('.work-map-step-title')].map(e=>e.textContent)`), order, 'model topic changes cannot reorder the shared thread')
|
|
68
|
+
await run(`document.querySelector('.work-map-step-button').click()`)
|
|
69
|
+
await run(`new Promise(resolve=>setTimeout(resolve,30))`)
|
|
70
|
+
assert.match(await run(`document.querySelector('.work-map-evidence').textContent`), /Full thinking/)
|
|
71
|
+
assert.match(await run(`document.querySelector('.work-map-source-text').textContent`), /Earlier context.*\n[\s\S]*Later context/)
|
|
72
|
+
assert.equal(await run(`document.querySelectorAll('.work-map-source-text mark').length`), 2)
|
|
73
|
+
assert.match(await run(`document.querySelector('mark.choice').textContent`), /Preserve the reader/)
|
|
74
|
+
assert.match(await run(`document.querySelector('mark.reason').textContent`), /does not interrupt/)
|
|
75
|
+
assert.match(await run(`document.querySelector('.work-map-step.follows-intent').textContent`), /Follows thinking/)
|
|
76
|
+
assert.equal(await run(`document.querySelectorAll('.work-map-step').length`), 29)
|
|
77
|
+
assert.equal(await run(`document.querySelector('select').value`), 'clef-flash')
|
|
78
|
+
await run(`document.querySelector('.work-map-step-button').click()`)
|
|
79
|
+
await run(`new Promise(resolve=>setTimeout(resolve,30))`)
|
|
80
|
+
assert(await run(`(()=>{const el=document.querySelector('.work-map-scroll');return el.scrollHeight-el.scrollTop-el.clientHeight<5})()`))
|
|
81
|
+
await run(`document.querySelector('.work-map-scroll').scrollTop=0`)
|
|
82
|
+
await run(`new Promise(resolve=>setTimeout(resolve,80))`)
|
|
83
|
+
await run(`window.emitMap({nodes:[...window.mapSnapshot().nodes,{...window.mapSnapshot().nodes.filter(n=>n.kind==='step').at(-1),id:'new-step'}]})`)
|
|
84
|
+
await run(`new Promise(resolve=>setTimeout(resolve,80))`)
|
|
85
|
+
assert.equal(await run(`document.querySelector('.work-map-scroll').scrollTop`), 0, 'updates preserve scroll position while reading history')
|
|
86
|
+
assert(await run(`!!document.querySelector('.work-map-jump')`))
|
|
87
|
+
await run(`document.querySelector('.work-map-branch-heading').click()`)
|
|
88
|
+
await run(`new Promise(resolve=>setTimeout(resolve,30))`)
|
|
89
|
+
assert.equal(await run(`document.querySelectorAll('.work-map-step').length`), 0)
|
|
90
|
+
await run(`document.querySelector('.work-map-branch-heading').click()`)
|
|
91
|
+
await run(`new Promise(resolve=>setTimeout(resolve,30))`)
|
|
92
|
+
await run(`document.querySelector('.work-map-step-button').click()`)
|
|
93
|
+
await run(`new Promise(resolve=>setTimeout(resolve,30))`)
|
|
94
|
+
assert.match(await run(`document.querySelector('.work-map-evidence').textContent`), /rollout-example\.jsonl/)
|
|
95
|
+
assert.equal(await run(`document.querySelectorAll('.work-map-step.failed').length`), 1)
|
|
96
|
+
await run(`document.querySelector('[role=switch]').click()`)
|
|
97
|
+
await run(`new Promise(resolve=>setTimeout(resolve,30))`)
|
|
98
|
+
assert.equal(await run(`document.querySelector('[role=switch]').getAttribute('aria-checked')`), 'false')
|
|
99
|
+
await run(`document.querySelector('[role=switch]').click()`)
|
|
100
|
+
await run(`new Promise(resolve=>setTimeout(resolve,30))`)
|
|
101
|
+
await run(`document.querySelector('.work-map-scroll').scrollTop=0`)
|
|
102
|
+
await run(`new Promise(resolve=>setTimeout(resolve,30))`)
|
|
103
|
+
assert(await run(`document.documentElement.scrollWidth<=window.innerWidth`), 'no horizontal overflow at panel width')
|
|
104
|
+
const screenshot = process.env.WORK_MAP_SCREENSHOT || path.join(repo, 'experiments/live-map/.cache/work-map-panel.png')
|
|
105
|
+
await fs.mkdir(path.dirname(screenshot), { recursive: true })
|
|
106
|
+
await fs.writeFile(screenshot, (await win.webContents.capturePage()).toPNG())
|
|
107
|
+
await run(`document.querySelector('.work-map-footer button').click()`)
|
|
108
|
+
await run(`new Promise(resolve=>setTimeout(resolve,30))`)
|
|
109
|
+
assert.equal(await run(`document.querySelectorAll('.work-map-goal').length`), 0)
|
|
110
|
+
assert(await run(`!!document.querySelector('.work-map-empty')`))
|
|
111
|
+
await run(`window.emitMap({thinkingStatus:'unavailable',nodes:window.exampleNodes.filter(n=>n.id!=='thought')})`)
|
|
112
|
+
await run(`new Promise(resolve=>setTimeout(resolve,30))`)
|
|
113
|
+
assert.match(await run(`document.querySelector('.work-map-thinking-status').textContent`), /omits readable thinking/)
|
|
114
|
+
assert.equal(await run(`document.querySelectorAll('.work-map-step').length`), 28, 'activity remains visible when thinking is omitted')
|
|
115
|
+
assert.equal(await run(`document.querySelectorAll('.work-map-intent').length`), 0, 'tool activity must not become invented decisions when thinking is omitted')
|
|
116
|
+
for (const state of ['checking', 'unavailable']) {
|
|
117
|
+
await run(`window.emitCapability({state:${JSON.stringify(state)},reason:'GPU unavailable'})`)
|
|
118
|
+
await run(`new Promise(resolve=>setTimeout(resolve,30))`)
|
|
119
|
+
assert(await run(`document.querySelector('[aria-label="Work map"]').disabled`))
|
|
120
|
+
assert(await run(`document.querySelector('[role=switch]').disabled`))
|
|
121
|
+
assert(await run(`document.querySelector('select').disabled`))
|
|
122
|
+
}
|
|
123
|
+
console.log('PASS unified chronological thread, salient excerpts and reasons, source receipts, action links, omitted-thinking notice, controls and scroll')
|
|
124
|
+
console.log('Screenshot:', screenshot)
|
|
125
|
+
} finally {
|
|
126
|
+
win?.destroy()
|
|
127
|
+
await fs.rm(temp, { recursive: true, force: true, maxRetries: 3 })
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
main().then(() => app.exit(0), err => { console.error(err); app.exit(1) })
|