@phenx-inc/ctlsurf 0.14.1 → 0.15.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 +36 -0
- package/THIRD-PARTY-NOTICES.md +82 -0
- package/out/headless/index.mjs +13 -3
- package/out/headless/index.mjs.map +2 -2
- package/out/main/index.js +27 -0
- package/out/preload/index.js +5 -0
- package/out/renderer/assets/{cssMode-bQYoCKra.js → cssMode-CJR4iD-G.js} +3 -3
- package/out/renderer/assets/{freemarker2-BS1ERQl_.js → freemarker2-BRI9ISq9.js} +1 -1
- package/out/renderer/assets/{handlebars-B-ZEHRLr.js → handlebars-3hL6XIZI.js} +1 -1
- package/out/renderer/assets/{html-lk9e1Lkl.js → html-Dd5_vBoP.js} +1 -1
- package/out/renderer/assets/{htmlMode-Ds1eMYgM.js → htmlMode-DgNtlKcG.js} +3 -3
- package/out/renderer/assets/{index-BpCORklm.css → index-3sFJEz7i.css} +787 -399
- package/out/renderer/assets/{index-BJNR4hRK.js → index-5azC4yNn.js} +421 -99
- package/out/renderer/assets/{javascript-D6OeziKX.js → javascript-D0vPLEJg.js} +2 -2
- package/out/renderer/assets/{jsonMode-t5SESE75.js → jsonMode-DBvR0zMu.js} +3 -3
- package/out/renderer/assets/{liquid-B5Wjh0p_.js → liquid-CLoc6Tx7.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-BEgiDCIW.js → lspLanguageFeatures-CiaMaQoY.js} +1 -1
- package/out/renderer/assets/{mdx-ByNAXh8t.js → mdx-DTsq6tlv.js} +1 -1
- package/out/renderer/assets/{python-CIFO0XQW.js → python-D04DpZDr.js} +1 -1
- package/out/renderer/assets/{razor-LXHhGcNO.js → razor-Di6CJ6s8.js} +1 -1
- package/out/renderer/assets/{tsMode-WiO8CDaI.js → tsMode-mfv6mHOv.js} +1 -1
- package/out/renderer/assets/{typescript-ZBQlq256.js → typescript-CDA0Hhef.js} +1 -1
- package/out/renderer/assets/{xml-DBaLJlQU.js → xml-BU0voRuq.js} +1 -1
- package/out/renderer/assets/{yaml-CId9skah.js → yaml-qzFWnaZJ.js} +1 -1
- package/out/renderer/index.html +60 -3
- package/package.json +13 -3
- package/src/main/index.ts +52 -1
- package/src/main/orchestrator.ts +8 -0
- package/src/preload/index.ts +10 -0
- package/src/renderer/App.tsx +2 -0
- package/src/renderer/components/CtlsurfPanel.tsx +1 -1
- package/src/renderer/components/EditorPanel.tsx +29 -1
- package/src/renderer/components/ObserverPanel.tsx +13 -6
- package/src/renderer/components/SettingsDialog.tsx +82 -0
- package/src/renderer/components/TerminalPanel.tsx +12 -22
- package/src/renderer/components/TicketPanel.tsx +17 -10
- package/src/renderer/index.html +58 -1
- package/src/renderer/lib/theme.ts +173 -0
- package/src/renderer/lib/themes.ts +126 -0
- package/src/renderer/styles.css +787 -399
|
@@ -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-CDA0Hhef.js";
|
|
2
|
+
import "./index-5azC4yNn.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-5azC4yNn.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-CiaMaQoY.js";
|
|
3
|
+
import { a, D, h, R, c, i, j, t, k } from "./lspLanguageFeatures-CiaMaQoY.js";
|
|
4
4
|
const STOP_WHEN_IDLE_FOR = 2 * 60 * 1e3;
|
|
5
5
|
class WorkerManager {
|
|
6
6
|
constructor(defaults) {
|
package/out/renderer/assets/{lspLanguageFeatures-BEgiDCIW.js → lspLanguageFeatures-CiaMaQoY.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-5azC4yNn.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-5azC4yNn.js";
|
|
2
2
|
class WorkerManager {
|
|
3
3
|
constructor(_modeId, _defaults) {
|
|
4
4
|
this._modeId = _modeId;
|
package/out/renderer/index.html
CHANGED
|
@@ -4,10 +4,42 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>ctlsurf-worker</title>
|
|
7
|
+
<!-- Stamp the theme before the first paint. This has to be an inline,
|
|
8
|
+
synchronous script in <head>: reading the pref over IPC (or waiting for
|
|
9
|
+
the bundle) would let one frame of the wrong theme through, which reads
|
|
10
|
+
as a flash every launch. lib/theme.ts owns the same key afterwards. -->
|
|
11
|
+
<script>
|
|
12
|
+
(function () {
|
|
13
|
+
var DARK = 'tokyo-night', LIGHT = 'tokyo-day'
|
|
14
|
+
var a = null
|
|
15
|
+
try {
|
|
16
|
+
var raw = localStorage.getItem('ctlsurf.appearance')
|
|
17
|
+
if (raw) a = JSON.parse(raw)
|
|
18
|
+
else {
|
|
19
|
+
// Migrate the original system/light/dark preference. lib/theme.ts
|
|
20
|
+
// performs the same mapping; both must agree or the first frame
|
|
21
|
+
// disagrees with the second.
|
|
22
|
+
var legacy = localStorage.getItem('ctlsurf.theme')
|
|
23
|
+
if (legacy === 'light') a = { sync: false, theme: LIGHT }
|
|
24
|
+
else if (legacy === 'dark') a = { sync: false, theme: DARK }
|
|
25
|
+
}
|
|
26
|
+
} catch (e) { /* private mode, or corrupt JSON */ }
|
|
27
|
+
var prefersLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches
|
|
28
|
+
var id
|
|
29
|
+
if (a && a.sync === false) id = a.theme
|
|
30
|
+
else if (a) id = prefersLight ? a.light : a.dark
|
|
31
|
+
else id = prefersLight ? LIGHT : DARK
|
|
32
|
+
document.documentElement.setAttribute('data-theme', id || (prefersLight ? LIGHT : DARK))
|
|
33
|
+
})()
|
|
34
|
+
</script>
|
|
7
35
|
<!-- Boot splash styles are inlined here so they paint on the first frame,
|
|
8
36
|
before the JS bundle and styles.css have loaded. Without this the window
|
|
9
|
-
shows an empty dark pane while React mounts and the startup IPC runs.
|
|
37
|
+
shows an empty dark pane while React mounts and the startup IPC runs.
|
|
38
|
+
They cannot use the tokens in styles.css for the same reason, so the two
|
|
39
|
+
palettes are repeated here — keep them in step with :root there. -->
|
|
10
40
|
<style>
|
|
41
|
+
:root { color-scheme: dark }
|
|
42
|
+
:root[data-theme='tokyo-day'], :root[data-theme='catppuccin-latte'] { color-scheme: light }
|
|
11
43
|
#boot-splash {
|
|
12
44
|
position: fixed;
|
|
13
45
|
inset: 0;
|
|
@@ -48,9 +80,34 @@
|
|
|
48
80
|
letter-spacing: 0.04em;
|
|
49
81
|
color: #565f89;
|
|
50
82
|
}
|
|
83
|
+
/* One rule per theme: the splash paints before styles.css exists, so it
|
|
84
|
+
cannot use the tokens. Keep in step with the :root blocks there. */
|
|
85
|
+
:root[data-theme='tokyo-day'] #boot-splash { background: #e9eaef; }
|
|
86
|
+
:root[data-theme='tokyo-day'] .boot-spinner {
|
|
87
|
+
border-color: rgba(46, 125, 233, 0.22);
|
|
88
|
+
border-top-color: #2e7de9;
|
|
89
|
+
}
|
|
90
|
+
:root[data-theme='tokyo-day'] .boot-splash-title { color: #1b1e33; }
|
|
91
|
+
:root[data-theme='tokyo-day'] .boot-splash-label { color: #6b7190; }
|
|
92
|
+
|
|
93
|
+
:root[data-theme='catppuccin-mocha'] #boot-splash { background: #1e1e2e; }
|
|
94
|
+
:root[data-theme='catppuccin-mocha'] .boot-spinner {
|
|
95
|
+
border-color: rgba(137, 180, 250, 0.22);
|
|
96
|
+
border-top-color: #89b4fa;
|
|
97
|
+
}
|
|
98
|
+
:root[data-theme='catppuccin-mocha'] .boot-splash-title { color: #cdd6f4; }
|
|
99
|
+
:root[data-theme='catppuccin-mocha'] .boot-splash-label { color: #7f849c; }
|
|
100
|
+
|
|
101
|
+
:root[data-theme='catppuccin-latte'] #boot-splash { background: #eff1f5; }
|
|
102
|
+
:root[data-theme='catppuccin-latte'] .boot-spinner {
|
|
103
|
+
border-color: rgba(30, 102, 245, 0.22);
|
|
104
|
+
border-top-color: #1e66f5;
|
|
105
|
+
}
|
|
106
|
+
:root[data-theme='catppuccin-latte'] .boot-splash-title { color: #2f3145; }
|
|
107
|
+
:root[data-theme='catppuccin-latte'] .boot-splash-label { color: #7c7f93; }
|
|
51
108
|
</style>
|
|
52
|
-
<script type="module" crossorigin src="./assets/index-
|
|
53
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
109
|
+
<script type="module" crossorigin src="./assets/index-5azC4yNn.js"></script>
|
|
110
|
+
<link rel="stylesheet" crossorigin href="./assets/index-3sFJEz7i.css">
|
|
54
111
|
</head>
|
|
55
112
|
<body>
|
|
56
113
|
<div id="boot-splash">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phenx-inc/ctlsurf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
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": {
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"src/",
|
|
16
16
|
"package.json",
|
|
17
17
|
"electron-vite.config.ts",
|
|
18
|
-
"tsconfig*.json"
|
|
18
|
+
"tsconfig*.json",
|
|
19
|
+
"THIRD-PARTY-NOTICES.md"
|
|
19
20
|
],
|
|
20
21
|
"scripts": {
|
|
21
22
|
"dev": "electron-vite dev",
|
|
@@ -36,7 +37,12 @@
|
|
|
36
37
|
"codex",
|
|
37
38
|
"electron",
|
|
38
39
|
"node-pty",
|
|
39
|
-
"xterm"
|
|
40
|
+
"xterm",
|
|
41
|
+
"ai-agent",
|
|
42
|
+
"mcp",
|
|
43
|
+
"agent-observability",
|
|
44
|
+
"provenance",
|
|
45
|
+
"time-tracking"
|
|
40
46
|
],
|
|
41
47
|
"license": "SEE LICENSE IN LICENSE",
|
|
42
48
|
"engines": {
|
|
@@ -74,5 +80,9 @@
|
|
|
74
80
|
"optionalDependencies": {
|
|
75
81
|
"bufferutil": "^4.1.0",
|
|
76
82
|
"utf-8-validate": "^6.0.6"
|
|
83
|
+
},
|
|
84
|
+
"homepage": "https://ctlsurf.com",
|
|
85
|
+
"bugs": {
|
|
86
|
+
"email": "phenx@phenx.io"
|
|
77
87
|
}
|
|
78
88
|
}
|
package/src/main/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { app, BrowserWindow, ipcMain, dialog, nativeImage, clipboard } from 'electron'
|
|
1
|
+
import { app, BrowserWindow, ipcMain, dialog, nativeImage, clipboard, nativeTheme } from 'electron'
|
|
2
2
|
import path from 'path'
|
|
3
3
|
import fs from 'fs'
|
|
4
4
|
|
|
@@ -233,12 +233,24 @@ const orchestrator = new Orchestrator(
|
|
|
233
233
|
// ─── Window ───────────────────────────────────────
|
|
234
234
|
|
|
235
235
|
function createWindow(): void {
|
|
236
|
+
// Replay last session's appearance choice before the window exists, so the
|
|
237
|
+
// frame Electron paints before the renderer loads is already the right
|
|
238
|
+
// colour. The renderer re-asserts this over app:setAppearance once booted.
|
|
239
|
+
const saved = orchestrator.settingsData
|
|
240
|
+
if (saved.themeSource === 'light' || saved.themeSource === 'dark' || saved.themeSource === 'system') {
|
|
241
|
+
nativeTheme.themeSource = saved.themeSource
|
|
242
|
+
}
|
|
243
|
+
const savedBg = typeof saved.themeBackground === 'string' && /^#[0-9a-fA-F]{6}$/.test(saved.themeBackground)
|
|
244
|
+
? saved.themeBackground
|
|
245
|
+
: null
|
|
246
|
+
|
|
236
247
|
mainWindow = new BrowserWindow({
|
|
237
248
|
width: 1400,
|
|
238
249
|
height: 900,
|
|
239
250
|
minWidth: 600,
|
|
240
251
|
minHeight: 400,
|
|
241
252
|
title: 'ctlsurf-worker',
|
|
253
|
+
backgroundColor: savedBg ?? (nativeTheme.shouldUseDarkColors ? '#1a1b26' : '#e9eaef'),
|
|
242
254
|
titleBarStyle: 'hiddenInset',
|
|
243
255
|
icon: path.join(__dirname, '../../resources/icon.png'),
|
|
244
256
|
webPreferences: {
|
|
@@ -388,6 +400,45 @@ ipcMain.handle('app:homePath', () => app.getPath('home'))
|
|
|
388
400
|
ipcMain.handle('app:cwd', () => process.env.CTLSURF_WORKER_CWD || process.cwd())
|
|
389
401
|
ipcMain.handle('app:projectName', () => orchestrator.projectName)
|
|
390
402
|
|
|
403
|
+
// The renderer owns the theme preference (see lib/theme.ts) and mirrors it here
|
|
404
|
+
// so the parts Chromium paints for us — native form controls, scrollbars, and
|
|
405
|
+
// the window background between loads — match the app chrome. Also remembered
|
|
406
|
+
// so the next launch can paint the right backgroundColor before the renderer
|
|
407
|
+
// has booted.
|
|
408
|
+
// The renderer owns the appearance preference (see renderer/lib/theme.ts) and
|
|
409
|
+
// mirrors it here for two reasons: so Chromium paints native form controls,
|
|
410
|
+
// scrollbars and the pre-load window background to match, and so createWindow
|
|
411
|
+
// can pick the right backgroundColor on the *next* launch, before the renderer
|
|
412
|
+
// has booted. It is also the recovery source if localStorage is ever wiped.
|
|
413
|
+
ipcMain.handle('app:getAppearance', () => ({
|
|
414
|
+
pref: orchestrator.settingsData.appearance ?? null,
|
|
415
|
+
}))
|
|
416
|
+
|
|
417
|
+
ipcMain.handle('app:setAppearance', (_e, data: {
|
|
418
|
+
pref?: unknown
|
|
419
|
+
source?: 'system' | 'light' | 'dark'
|
|
420
|
+
background?: string
|
|
421
|
+
}) => {
|
|
422
|
+
const source = data?.source
|
|
423
|
+
if (source !== 'system' && source !== 'light' && source !== 'dark') return { ok: false }
|
|
424
|
+
nativeTheme.themeSource = source
|
|
425
|
+
|
|
426
|
+
const settings = orchestrator.settingsData
|
|
427
|
+
if (data.pref && typeof data.pref === 'object') {
|
|
428
|
+
settings.appearance = data.pref as Record<string, unknown>
|
|
429
|
+
}
|
|
430
|
+
settings.themeSource = source
|
|
431
|
+
// Only trust a plain hex colour — this value is handed straight to Electron.
|
|
432
|
+
if (typeof data.background === 'string' && /^#[0-9a-fA-F]{6}$/.test(data.background)) {
|
|
433
|
+
settings.themeBackground = data.background
|
|
434
|
+
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
435
|
+
mainWindow.setBackgroundColor(data.background)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
orchestrator.saveSettings()
|
|
439
|
+
return { ok: true }
|
|
440
|
+
})
|
|
441
|
+
|
|
391
442
|
ipcMain.handle('app:browseCwd', async () => {
|
|
392
443
|
if (!mainWindow) return null
|
|
393
444
|
const result = await dialog.showOpenDialog(mainWindow, {
|
package/src/main/orchestrator.ts
CHANGED
|
@@ -43,6 +43,14 @@ export interface SettingsData {
|
|
|
43
43
|
// src/main/observer/. Requires an Anthropic API key for Tiers 1–2;
|
|
44
44
|
// without one it still records the ledger, Tier-0 tags, and spans.
|
|
45
45
|
observer?: ObserverSettings
|
|
46
|
+
// Mirror of the renderer's appearance preference (localStorage owns the
|
|
47
|
+
// authoritative copy — index.html has to read it before first paint). Kept
|
|
48
|
+
// here so createWindow can pick the right backgroundColor on the next launch
|
|
49
|
+
// before the renderer boots, and so the choice survives a wiped localStorage.
|
|
50
|
+
// Shape is owned by renderer/lib/theme.ts; the main process only round-trips it.
|
|
51
|
+
appearance?: Record<string, unknown>
|
|
52
|
+
themeSource?: 'system' | 'light' | 'dark'
|
|
53
|
+
themeBackground?: string
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
export interface OrchestratorEvents {
|
package/src/preload/index.ts
CHANGED
|
@@ -79,6 +79,16 @@ const api = {
|
|
|
79
79
|
},
|
|
80
80
|
browseCwd: (): Promise<string | null> =>
|
|
81
81
|
ipcRenderer.invoke('app:browseCwd'),
|
|
82
|
+
// Mirror the renderer's appearance preference onto Chromium's nativeTheme and
|
|
83
|
+
// persist it. localStorage stays the source of truth (see renderer/lib/theme.ts);
|
|
84
|
+
// this copy backs the pre-load window background and localStorage recovery.
|
|
85
|
+
setAppearance: (data: {
|
|
86
|
+
pref: unknown
|
|
87
|
+
source: 'system' | 'light' | 'dark'
|
|
88
|
+
background: string
|
|
89
|
+
}): Promise<{ ok: boolean }> => ipcRenderer.invoke('app:setAppearance', data),
|
|
90
|
+
getAppearance: (): Promise<{ pref: unknown }> =>
|
|
91
|
+
ipcRenderer.invoke('app:getAppearance'),
|
|
82
92
|
|
|
83
93
|
// Clipboard — write a rich-HTML table (+ plain-text fallback) for email paste.
|
|
84
94
|
// `ok` reflects a read-back verification in the main process, not just that the
|
package/src/renderer/App.tsx
CHANGED
|
@@ -50,6 +50,8 @@ declare global {
|
|
|
50
50
|
getProjectName: () => Promise<string | null>
|
|
51
51
|
onProjectChanged: (callback: (name: string | null) => void) => () => void
|
|
52
52
|
browseCwd: () => Promise<string | null>
|
|
53
|
+
setAppearance?: (data: { pref: unknown; source: 'system' | 'light' | 'dark'; background: string }) => Promise<{ ok: boolean }>
|
|
54
|
+
getAppearance?: () => Promise<{ pref: unknown }>
|
|
53
55
|
getSetting: (key: string) => Promise<string | null>
|
|
54
56
|
setSetting: (key: string, value: string) => Promise<{ ok: boolean }>
|
|
55
57
|
getAllSettings: () => Promise<Record<string, string | null>>
|
|
@@ -190,7 +190,7 @@ export function CtlsurfPanel({ navigate }: CtlsurfPanelProps) {
|
|
|
190
190
|
/>
|
|
191
191
|
) : (
|
|
192
192
|
<div className="ctlsurf-webview-loading">
|
|
193
|
-
<span style={{ color: '
|
|
193
|
+
<span style={{ color: 'var(--text-muted)', fontSize: '13px' }}>Loading ctlsurf...</span>
|
|
194
194
|
</div>
|
|
195
195
|
)}
|
|
196
196
|
</div>
|
|
@@ -6,6 +6,8 @@ import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
|
|
|
6
6
|
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
|
|
7
7
|
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'
|
|
8
8
|
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
|
|
9
|
+
import { useAppearance } from '../lib/theme'
|
|
10
|
+
import { THEMES } from '../lib/themes'
|
|
9
11
|
|
|
10
12
|
// Configure Monaco workers for Vite
|
|
11
13
|
self.MonacoEnvironment = {
|
|
@@ -21,6 +23,31 @@ self.MonacoEnvironment = {
|
|
|
21
23
|
// Use local monaco instead of CDN
|
|
22
24
|
loader.config({ monaco })
|
|
23
25
|
|
|
26
|
+
// One Monaco theme per scheme. Monaco only ships 'vs' / 'vs-dark', so each
|
|
27
|
+
// scheme inherits the matching base (keeping its syntax highlighting) and
|
|
28
|
+
// overrides the surfaces, which are what would otherwise clash with the
|
|
29
|
+
// surrounding chrome. Registered at module load — Monaco resolves a theme by
|
|
30
|
+
// name at render time, so these must exist before the first <Editor> mounts.
|
|
31
|
+
for (const t of THEMES) {
|
|
32
|
+
monaco.editor.defineTheme(`ctlsurf-${t.id}`, {
|
|
33
|
+
base: t.editor.base,
|
|
34
|
+
inherit: true,
|
|
35
|
+
rules: [],
|
|
36
|
+
colors: {
|
|
37
|
+
'editor.background': t.editor.background,
|
|
38
|
+
'editor.foreground': t.editor.foreground,
|
|
39
|
+
'editor.lineHighlightBackground': t.editor.lineHighlight,
|
|
40
|
+
'editor.selectionBackground': t.editor.selection,
|
|
41
|
+
'editorLineNumber.foreground': t.swatch.surface,
|
|
42
|
+
'editorLineNumber.activeForeground': t.swatch.accent,
|
|
43
|
+
'editorGutter.background': t.editor.background,
|
|
44
|
+
'editorWidget.background': t.editor.lineHighlight,
|
|
45
|
+
'editorCursor.foreground': t.terminal.cursor,
|
|
46
|
+
'scrollbarSlider.background': `${t.swatch.surface}99`,
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
24
51
|
interface FileEntry {
|
|
25
52
|
name: string
|
|
26
53
|
path: string
|
|
@@ -86,6 +113,7 @@ interface EditorPanelProps {
|
|
|
86
113
|
}
|
|
87
114
|
|
|
88
115
|
export function EditorPanel({ cwd }: EditorPanelProps) {
|
|
116
|
+
const { theme } = useAppearance()
|
|
89
117
|
const [rootEntries, setRootEntries] = useState<FileEntry[]>([])
|
|
90
118
|
const [openFile, setOpenFile] = useState<string | null>(null)
|
|
91
119
|
const [fileContent, setFileContent] = useState<string>('')
|
|
@@ -208,7 +236,7 @@ export function EditorPanel({ cwd }: EditorPanelProps) {
|
|
|
208
236
|
language={getLanguage(openFile)}
|
|
209
237
|
value={fileContent}
|
|
210
238
|
onChange={(val) => { setFileContent(val || ''); setModified(true) }}
|
|
211
|
-
theme=
|
|
239
|
+
theme={`ctlsurf-${theme.id}`}
|
|
212
240
|
options={{
|
|
213
241
|
minimap: { enabled: false },
|
|
214
242
|
fontSize: 13,
|
|
@@ -43,13 +43,15 @@ const SENSITIVITY_LEVELS: Array<{ value: Sensitivity; label: string; hint: strin
|
|
|
43
43
|
{ value: 'all', label: 'All signals', hint: 'Everything, including low-confidence observations' },
|
|
44
44
|
]
|
|
45
45
|
|
|
46
|
+
// Resolved from the token layer so both themes get a legible variant; see the
|
|
47
|
+
// :root blocks in styles.css.
|
|
46
48
|
const SEVERITY_COLORS: Record<string, string> = {
|
|
47
|
-
info: '
|
|
48
|
-
warn: '
|
|
49
|
-
high: '
|
|
50
|
-
critical: '
|
|
49
|
+
info: 'var(--accent)',
|
|
50
|
+
warn: 'var(--warning)',
|
|
51
|
+
high: 'var(--sev-high)',
|
|
52
|
+
critical: 'var(--danger)',
|
|
51
53
|
}
|
|
52
|
-
const DIGEST_COLOR = '
|
|
54
|
+
const DIGEST_COLOR = 'var(--sev-digest)'
|
|
53
55
|
|
|
54
56
|
function EyeIcon({ size = 16 }: { size?: number }) {
|
|
55
57
|
return (
|
|
@@ -67,9 +69,14 @@ function formatTime(iso: string): string {
|
|
|
67
69
|
return d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
// `${color}55` only worked while the colours were hex literals; token
|
|
73
|
+
// references have to be blended for real.
|
|
74
|
+
const tint = (color: string, pct: number): string =>
|
|
75
|
+
`color-mix(in srgb, ${color} ${pct}%, transparent)`
|
|
76
|
+
|
|
70
77
|
function Chip({ color, label }: { color: string; label: string }) {
|
|
71
78
|
return (
|
|
72
|
-
<span className="obs-chip" style={{ color, borderColor:
|
|
79
|
+
<span className="obs-chip" style={{ color, borderColor: tint(color, 33), background: tint(color, 8) }}>
|
|
73
80
|
<span className="obs-chip-dot" style={{ background: color }} />
|
|
74
81
|
{label}
|
|
75
82
|
</span>
|
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react'
|
|
2
|
+
import { useAppearance } from '../lib/theme'
|
|
3
|
+
import { THEMES, themesByMode, type ThemeDef } from '../lib/themes'
|
|
4
|
+
|
|
5
|
+
// A tile per scheme. The swatch is drawn from the theme's own literal colours
|
|
6
|
+
// (not the active tokens) so each preview shows what it actually is.
|
|
7
|
+
function ThemeGrid({ themes, value, onChange }: {
|
|
8
|
+
themes: ThemeDef[]
|
|
9
|
+
value: string
|
|
10
|
+
onChange: (id: string) => void
|
|
11
|
+
}) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="theme-options" role="radiogroup">
|
|
14
|
+
{themes.map(t => (
|
|
15
|
+
<button
|
|
16
|
+
key={t.id}
|
|
17
|
+
type="button"
|
|
18
|
+
role="radio"
|
|
19
|
+
aria-checked={value === t.id}
|
|
20
|
+
title={t.name}
|
|
21
|
+
className={`theme-option ${value === t.id ? 'active' : ''}`}
|
|
22
|
+
onClick={() => onChange(t.id)}
|
|
23
|
+
>
|
|
24
|
+
<span className="theme-swatch" style={{ background: t.swatch.bg }} aria-hidden="true">
|
|
25
|
+
<span className="theme-swatch-bar" style={{ background: t.swatch.surface }} />
|
|
26
|
+
<span className="theme-swatch-dot" style={{ background: t.swatch.accent }} />
|
|
27
|
+
<span className="theme-swatch-line" style={{ background: t.swatch.text }} />
|
|
28
|
+
</span>
|
|
29
|
+
<span className="theme-option-label">{t.name}</span>
|
|
30
|
+
</button>
|
|
31
|
+
))}
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
2
35
|
|
|
3
36
|
interface ProfileSummary {
|
|
4
37
|
id: string
|
|
@@ -21,6 +54,7 @@ interface SettingsDialogProps {
|
|
|
21
54
|
}
|
|
22
55
|
|
|
23
56
|
export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
|
|
57
|
+
const { appearance, theme, update: updateAppearance } = useAppearance()
|
|
24
58
|
const [profileList, setProfileList] = useState<ProfileListResponse | null>(null)
|
|
25
59
|
const [activeProfileId, setActiveProfileId] = useState('production')
|
|
26
60
|
const [editingId, setEditingId] = useState<string | null>(null)
|
|
@@ -157,6 +191,54 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
|
|
|
157
191
|
<div className="settings-dialog" onClick={e => e.stopPropagation()}>
|
|
158
192
|
<h3>Settings</h3>
|
|
159
193
|
|
|
194
|
+
{!editingId && (
|
|
195
|
+
// ─── Appearance (global) ───────────────
|
|
196
|
+
<div className="settings-section">
|
|
197
|
+
<h4>Appearance</h4>
|
|
198
|
+
<p className="settings-hint">
|
|
199
|
+
Applies everywhere, including the terminal and code editor.
|
|
200
|
+
</p>
|
|
201
|
+
|
|
202
|
+
<label className="settings-checkbox">
|
|
203
|
+
<input
|
|
204
|
+
type="checkbox"
|
|
205
|
+
checked={appearance.sync}
|
|
206
|
+
onChange={e => updateAppearance({ sync: e.target.checked })}
|
|
207
|
+
/>
|
|
208
|
+
<span>Sync with system appearance</span>
|
|
209
|
+
<span className="settings-hint">
|
|
210
|
+
Switches between your light and dark picks as the OS changes.
|
|
211
|
+
</span>
|
|
212
|
+
</label>
|
|
213
|
+
|
|
214
|
+
{appearance.sync ? (
|
|
215
|
+
<>
|
|
216
|
+
<div className="theme-group-label">Light</div>
|
|
217
|
+
<ThemeGrid
|
|
218
|
+
themes={themesByMode('light')}
|
|
219
|
+
value={appearance.light}
|
|
220
|
+
onChange={id => updateAppearance({ light: id })}
|
|
221
|
+
/>
|
|
222
|
+
<div className="theme-group-label">Dark</div>
|
|
223
|
+
<ThemeGrid
|
|
224
|
+
themes={themesByMode('dark')}
|
|
225
|
+
value={appearance.dark}
|
|
226
|
+
onChange={id => updateAppearance({ dark: id })}
|
|
227
|
+
/>
|
|
228
|
+
<span className="settings-hint">
|
|
229
|
+
Currently {theme.name}.
|
|
230
|
+
</span>
|
|
231
|
+
</>
|
|
232
|
+
) : (
|
|
233
|
+
<ThemeGrid
|
|
234
|
+
themes={THEMES}
|
|
235
|
+
value={appearance.theme}
|
|
236
|
+
onChange={id => updateAppearance({ theme: id })}
|
|
237
|
+
/>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
|
|
160
242
|
{editingId ? (
|
|
161
243
|
// ─── Profile Edit Form ─────────────────
|
|
162
244
|
<div className="settings-section">
|
|
@@ -5,6 +5,7 @@ import { WebLinksAddon } from '@xterm/addon-web-links'
|
|
|
5
5
|
import '@xterm/xterm/css/xterm.css'
|
|
6
6
|
import { convertToEmailTable } from '../lib/tableToHtml'
|
|
7
7
|
import { speech } from '../lib/speech'
|
|
8
|
+
import { currentTheme, subscribeTheme } from '../lib/theme'
|
|
8
9
|
|
|
9
10
|
// Show a transient toast in the active terminal panel. Broadcast as a window
|
|
10
11
|
// event so module-level helpers (which have no component scope) can trigger it.
|
|
@@ -137,6 +138,16 @@ type TermState = {
|
|
|
137
138
|
|
|
138
139
|
const _terminals = new Map<string, TermState>()
|
|
139
140
|
|
|
141
|
+
// Terminals are cached here and outlive the components that render them, so
|
|
142
|
+
// the palette swap is driven from module scope rather than a component effect.
|
|
143
|
+
// xterm paints to a canvas, so the CSS tokens in styles.css cannot reach it —
|
|
144
|
+
// each theme carries its own 16-colour palette in lib/themes.ts.
|
|
145
|
+
subscribeTheme((theme) => {
|
|
146
|
+
for (const state of _terminals.values()) {
|
|
147
|
+
state.terminal.options.theme = theme.terminal
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
|
|
140
151
|
// Within a couple of lines of the bottom still counts as "at bottom", so a
|
|
141
152
|
// small redraw delta doesn't strand the view just shy of the end.
|
|
142
153
|
function isAtBottom(terminal: Terminal): boolean {
|
|
@@ -197,28 +208,7 @@ function getOrCreateTerminal(tabId: string, onExit: (tabId: string) => void): {
|
|
|
197
208
|
macOptionClickForcesSelection: true,
|
|
198
209
|
fontSize: 14,
|
|
199
210
|
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', monospace",
|
|
200
|
-
theme:
|
|
201
|
-
background: '#1a1b26',
|
|
202
|
-
foreground: '#a9b1d6',
|
|
203
|
-
cursor: '#c0caf5',
|
|
204
|
-
selectionBackground: '#33467C',
|
|
205
|
-
black: '#15161e',
|
|
206
|
-
red: '#f7768e',
|
|
207
|
-
green: '#9ece6a',
|
|
208
|
-
yellow: '#e0af68',
|
|
209
|
-
blue: '#7aa2f7',
|
|
210
|
-
magenta: '#bb9af7',
|
|
211
|
-
cyan: '#7dcfff',
|
|
212
|
-
white: '#a9b1d6',
|
|
213
|
-
brightBlack: '#414868',
|
|
214
|
-
brightRed: '#f7768e',
|
|
215
|
-
brightGreen: '#9ece6a',
|
|
216
|
-
brightYellow: '#e0af68',
|
|
217
|
-
brightBlue: '#7aa2f7',
|
|
218
|
-
brightMagenta: '#bb9af7',
|
|
219
|
-
brightCyan: '#7dcfff',
|
|
220
|
-
brightWhite: '#c0caf5'
|
|
221
|
-
},
|
|
211
|
+
theme: currentTheme().terminal,
|
|
222
212
|
scrollback: 10000,
|
|
223
213
|
scrollOnUserInput: true,
|
|
224
214
|
allowTransparency: false
|
|
@@ -2,20 +2,27 @@ import { useState, useEffect, useCallback } from 'react'
|
|
|
2
2
|
|
|
3
3
|
interface Option { value: string; color: string }
|
|
4
4
|
|
|
5
|
+
// Token references rather than literals, so the pills follow the theme. They
|
|
6
|
+
// are also alpha-blended below via color-mix, which needs a var() to mix into.
|
|
5
7
|
const STATUS_OPTIONS: Option[] = [
|
|
6
|
-
{ value: 'Open', color: '
|
|
7
|
-
{ value: 'In Progress', color: '
|
|
8
|
-
{ value: 'Blocked', color: '
|
|
9
|
-
{ value: 'Done', color: '
|
|
8
|
+
{ value: 'Open', color: 'var(--accent)' },
|
|
9
|
+
{ value: 'In Progress', color: 'var(--warning)' },
|
|
10
|
+
{ value: 'Blocked', color: 'var(--danger)' },
|
|
11
|
+
{ value: 'Done', color: 'var(--success)' },
|
|
10
12
|
]
|
|
11
13
|
const PRIORITY_OPTIONS: Option[] = [
|
|
12
|
-
{ value: 'Low', color: '
|
|
13
|
-
{ value: 'Med', color: '
|
|
14
|
-
{ value: 'High', color: '
|
|
14
|
+
{ value: 'Low', color: 'var(--text-muted)' },
|
|
15
|
+
{ value: 'Med', color: 'var(--warning)' },
|
|
16
|
+
{ value: 'High', color: 'var(--danger)' },
|
|
15
17
|
]
|
|
16
18
|
|
|
17
19
|
const colorOf = (opts: Option[], value: string): string =>
|
|
18
|
-
opts.find(o => o.value === value)?.color ?? '
|
|
20
|
+
opts.find(o => o.value === value)?.color ?? 'var(--text-muted)'
|
|
21
|
+
|
|
22
|
+
// `${color}1f` used to work because the colours were hex literals; tokens need
|
|
23
|
+
// a real blend.
|
|
24
|
+
const tint = (color: string, pct: number): string =>
|
|
25
|
+
`color-mix(in srgb, ${color} ${pct}%, transparent)`
|
|
19
26
|
|
|
20
27
|
interface Ticket {
|
|
21
28
|
id: string
|
|
@@ -100,7 +107,7 @@ function Segmented({ options, value, onChange }: SegmentedProps) {
|
|
|
100
107
|
key={o.value}
|
|
101
108
|
type="button"
|
|
102
109
|
className={`ticket-seg ${active ? 'active' : ''}`}
|
|
103
|
-
style={active ? { borderColor: o.color, color: o.color, background:
|
|
110
|
+
style={active ? { borderColor: o.color, color: o.color, background: tint(o.color, 12) } : undefined}
|
|
104
111
|
onClick={() => onChange(o.value)}
|
|
105
112
|
>
|
|
106
113
|
<span className="ticket-seg-dot" style={{ background: o.color }} />
|
|
@@ -132,7 +139,7 @@ function TicketCard({ ticket, onClick, onSend }: { ticket: Ticket; onClick: () =
|
|
|
132
139
|
</div>
|
|
133
140
|
<span
|
|
134
141
|
className="ticket-card-pri"
|
|
135
|
-
style={{ color: colorOf(PRIORITY_OPTIONS, ticket.priority), borderColor:
|
|
142
|
+
style={{ color: colorOf(PRIORITY_OPTIONS, ticket.priority), borderColor: tint(colorOf(PRIORITY_OPTIONS, ticket.priority), 40) }}
|
|
136
143
|
>
|
|
137
144
|
{ticket.priority}
|
|
138
145
|
</span>
|