arkaos 3.69.0 → 3.70.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/VERSION CHANGED
@@ -1 +1 @@
1
- 3.69.0
1
+ 3.70.0
@@ -9,10 +9,12 @@ import { FitAddon } from '@xterm/addon-fit'
9
9
  import { WebLinksAddon } from '@xterm/addon-web-links'
10
10
  import { SearchAddon } from '@xterm/addon-search'
11
11
  import '@xterm/xterm/css/xterm.css'
12
+ import type { XtermTheme } from '~/composables/useTerminalThemes'
12
13
 
13
14
  interface Props {
14
15
  session?: ReturnType<typeof useTerminalSession>
15
16
  onInputLine?: (line: string) => void
17
+ theme?: XtermTheme
16
18
  }
17
19
  const props = defineProps<Props>()
18
20
 
@@ -24,29 +26,10 @@ const search = shallowRef<SearchAddon | null>(null)
24
26
 
25
27
  const decoder = new TextDecoder('utf-8', { fatal: false })
26
28
 
27
- const themeArkaOSDark = {
28
- background: '#0a0a0f',
29
- foreground: '#e6e6f0',
30
- cursor: '#7dd3fc',
31
- cursorAccent: '#0a0a0f',
32
- selectionBackground: '#1e3a5f',
33
- black: '#0a0a0f',
34
- red: '#f87171',
35
- green: '#86efac',
36
- yellow: '#fde68a',
37
- blue: '#7dd3fc',
38
- magenta: '#f0abfc',
39
- cyan: '#67e8f9',
40
- white: '#e6e6f0',
41
- brightBlack: '#3f3f46',
42
- brightRed: '#fca5a5',
43
- brightGreen: '#bbf7d0',
44
- brightYellow: '#fef3c7',
45
- brightBlue: '#bae6fd',
46
- brightMagenta: '#f5d0fe',
47
- brightCyan: '#a5f3fc',
48
- brightWhite: '#fafafa',
49
- }
29
+ // PR99d v3.70.0 — theme comes from prop or from the composable
30
+ // default (operator's choice stored in localStorage).
31
+ const { activeTheme } = useTerminalThemes()
32
+ const effectiveTheme = computed(() => props.theme ?? activeTheme.value)
50
33
 
51
34
  let unsubscribeOutput: (() => void) | null = null
52
35
  let resizeObserver: ResizeObserver | null = null
@@ -60,9 +43,14 @@ onMounted(async () => {
60
43
  fontSize: 13,
61
44
  lineHeight: 1.2,
62
45
  scrollback: 5000,
63
- theme: themeArkaOSDark,
46
+ theme: effectiveTheme.value,
64
47
  allowProposedApi: true,
65
48
  })
49
+
50
+ // React to theme switches without remounting.
51
+ watch(effectiveTheme, (next) => {
52
+ if (term.value) term.value.options.theme = next
53
+ }, { deep: true })
66
54
  const fitAddon = new FitAddon()
67
55
  const searchAddon = new SearchAddon()
68
56
  t.loadAddon(fitAddon)
@@ -0,0 +1,190 @@
1
+ // PR99d v3.70.0 — theme presets for xterm.js.
2
+ //
3
+ // Stored as plain xterm.js ITheme objects. The active theme name lives
4
+ // in localStorage so it survives reloads. ArkaOS Dark is the default
5
+ // and is tuned to the dashboard's primary color.
6
+
7
+ export interface XtermTheme {
8
+ background: string
9
+ foreground: string
10
+ cursor: string
11
+ cursorAccent: string
12
+ selectionBackground: string
13
+ black: string
14
+ red: string
15
+ green: string
16
+ yellow: string
17
+ blue: string
18
+ magenta: string
19
+ cyan: string
20
+ white: string
21
+ brightBlack: string
22
+ brightRed: string
23
+ brightGreen: string
24
+ brightYellow: string
25
+ brightBlue: string
26
+ brightMagenta: string
27
+ brightCyan: string
28
+ brightWhite: string
29
+ }
30
+
31
+ export const TERMINAL_THEMES: Record<string, XtermTheme> = {
32
+ 'arkaos-dark': {
33
+ background: '#0a0a0f',
34
+ foreground: '#e6e6f0',
35
+ cursor: '#7dd3fc',
36
+ cursorAccent: '#0a0a0f',
37
+ selectionBackground: '#1e3a5f',
38
+ black: '#0a0a0f',
39
+ red: '#f87171',
40
+ green: '#86efac',
41
+ yellow: '#fde68a',
42
+ blue: '#7dd3fc',
43
+ magenta: '#f0abfc',
44
+ cyan: '#67e8f9',
45
+ white: '#e6e6f0',
46
+ brightBlack: '#3f3f46',
47
+ brightRed: '#fca5a5',
48
+ brightGreen: '#bbf7d0',
49
+ brightYellow: '#fef3c7',
50
+ brightBlue: '#bae6fd',
51
+ brightMagenta: '#f5d0fe',
52
+ brightCyan: '#a5f3fc',
53
+ brightWhite: '#fafafa',
54
+ },
55
+ dracula: {
56
+ background: '#282a36',
57
+ foreground: '#f8f8f2',
58
+ cursor: '#f8f8f2',
59
+ cursorAccent: '#282a36',
60
+ selectionBackground: '#44475a',
61
+ black: '#21222c',
62
+ red: '#ff5555',
63
+ green: '#50fa7b',
64
+ yellow: '#f1fa8c',
65
+ blue: '#bd93f9',
66
+ magenta: '#ff79c6',
67
+ cyan: '#8be9fd',
68
+ white: '#f8f8f2',
69
+ brightBlack: '#6272a4',
70
+ brightRed: '#ff6e6e',
71
+ brightGreen: '#69ff94',
72
+ brightYellow: '#ffffa5',
73
+ brightBlue: '#d6acff',
74
+ brightMagenta: '#ff92df',
75
+ brightCyan: '#a4ffff',
76
+ brightWhite: '#ffffff',
77
+ },
78
+ 'solarized-dark': {
79
+ background: '#002b36',
80
+ foreground: '#839496',
81
+ cursor: '#93a1a1',
82
+ cursorAccent: '#002b36',
83
+ selectionBackground: '#073642',
84
+ black: '#073642',
85
+ red: '#dc322f',
86
+ green: '#859900',
87
+ yellow: '#b58900',
88
+ blue: '#268bd2',
89
+ magenta: '#d33682',
90
+ cyan: '#2aa198',
91
+ white: '#eee8d5',
92
+ brightBlack: '#586e75',
93
+ brightRed: '#cb4b16',
94
+ brightGreen: '#586e75',
95
+ brightYellow: '#657b83',
96
+ brightBlue: '#839496',
97
+ brightMagenta: '#6c71c4',
98
+ brightCyan: '#93a1a1',
99
+ brightWhite: '#fdf6e3',
100
+ },
101
+ 'solarized-light': {
102
+ background: '#fdf6e3',
103
+ foreground: '#657b83',
104
+ cursor: '#586e75',
105
+ cursorAccent: '#fdf6e3',
106
+ selectionBackground: '#eee8d5',
107
+ black: '#073642',
108
+ red: '#dc322f',
109
+ green: '#859900',
110
+ yellow: '#b58900',
111
+ blue: '#268bd2',
112
+ magenta: '#d33682',
113
+ cyan: '#2aa198',
114
+ white: '#eee8d5',
115
+ brightBlack: '#002b36',
116
+ brightRed: '#cb4b16',
117
+ brightGreen: '#586e75',
118
+ brightYellow: '#657b83',
119
+ brightBlue: '#839496',
120
+ brightMagenta: '#6c71c4',
121
+ brightCyan: '#93a1a1',
122
+ brightWhite: '#fdf6e3',
123
+ },
124
+ nord: {
125
+ background: '#2e3440',
126
+ foreground: '#d8dee9',
127
+ cursor: '#d8dee9',
128
+ cursorAccent: '#2e3440',
129
+ selectionBackground: '#434c5e',
130
+ black: '#3b4252',
131
+ red: '#bf616a',
132
+ green: '#a3be8c',
133
+ yellow: '#ebcb8b',
134
+ blue: '#81a1c1',
135
+ magenta: '#b48ead',
136
+ cyan: '#88c0d0',
137
+ white: '#e5e9f0',
138
+ brightBlack: '#4c566a',
139
+ brightRed: '#bf616a',
140
+ brightGreen: '#a3be8c',
141
+ brightYellow: '#ebcb8b',
142
+ brightBlue: '#81a1c1',
143
+ brightMagenta: '#b48ead',
144
+ brightCyan: '#8fbcbb',
145
+ brightWhite: '#eceff4',
146
+ },
147
+ }
148
+
149
+ export const THEME_LABELS: Record<string, string> = {
150
+ 'arkaos-dark': 'ArkaOS Dark',
151
+ dracula: 'Dracula',
152
+ 'solarized-dark': 'Solarized Dark',
153
+ 'solarized-light': 'Solarized Light',
154
+ nord: 'Nord',
155
+ }
156
+
157
+ const STORAGE_KEY = 'arka-terminal-theme'
158
+ const DEFAULT_THEME = 'arkaos-dark'
159
+
160
+ export function useTerminalThemes() {
161
+ const themeName = useState<string>('terminal-theme', () => {
162
+ if (typeof localStorage === 'undefined') return DEFAULT_THEME
163
+ return localStorage.getItem(STORAGE_KEY) || DEFAULT_THEME
164
+ })
165
+
166
+ function setTheme(name: string) {
167
+ if (!TERMINAL_THEMES[name]) return
168
+ themeName.value = name
169
+ try {
170
+ localStorage.setItem(STORAGE_KEY, name)
171
+ } catch {
172
+ // ignore quota
173
+ }
174
+ }
175
+
176
+ const activeTheme = computed<XtermTheme>(
177
+ () => TERMINAL_THEMES[themeName.value] ?? TERMINAL_THEMES[DEFAULT_THEME]!,
178
+ )
179
+
180
+ const options = computed(() =>
181
+ Object.entries(THEME_LABELS).map(([value, label]) => ({ value, label })),
182
+ )
183
+
184
+ return {
185
+ themeName,
186
+ activeTheme,
187
+ setTheme,
188
+ options,
189
+ }
190
+ }
@@ -55,6 +55,29 @@ function recordCommand(cmd: string) {
55
55
  }
56
56
  }
57
57
 
58
+ // PR99d v3.70.0 — theme picker + Ctrl+R history search.
59
+ const { themeName, setTheme, options: themeOptions } = useTerminalThemes()
60
+ const searchOpen = ref(false)
61
+ const searchQuery = ref('')
62
+
63
+ const searchResults = computed(() => {
64
+ const q = searchQuery.value.trim().toLowerCase()
65
+ if (!q) return history.value.slice(0, 30)
66
+ return history.value
67
+ .filter((h) => h.cmd.toLowerCase().includes(q))
68
+ .slice(0, 30)
69
+ })
70
+
71
+ function openSearch() {
72
+ searchOpen.value = true
73
+ searchQuery.value = ''
74
+ }
75
+
76
+ function pickFromSearch(cmd: string) {
77
+ activeTab.value?.session.sendInput(cmd)
78
+ searchOpen.value = false
79
+ }
80
+
58
81
  const editingTabId = ref<string | null>(null)
59
82
  const renameDraft = ref('')
60
83
 
@@ -94,6 +117,7 @@ defineShortcuts({
94
117
  },
95
118
  usingInput: false,
96
119
  },
120
+ ctrl_r: { handler: openSearch, usingInput: false },
97
121
  meta_1: { handler: () => switchByIndex(0), usingInput: false },
98
122
  meta_2: { handler: () => switchByIndex(1), usingInput: false },
99
123
  meta_3: { handler: () => switchByIndex(2), usingInput: false },
@@ -137,6 +161,22 @@ const showHistory = ref(false)
137
161
  <UIcon name="i-lucide-shield" class="size-3 mr-1" />
138
162
  localhost only
139
163
  </UBadge>
164
+ <USelect
165
+ :model-value="themeName"
166
+ :items="themeOptions"
167
+ size="xs"
168
+ class="w-44"
169
+ @update:model-value="setTheme($event as string)"
170
+ />
171
+ <UButton
172
+ size="xs"
173
+ variant="ghost"
174
+ icon="i-lucide-search"
175
+ title="Ctrl+R — search history"
176
+ @click="openSearch"
177
+ >
178
+ ⌃R
179
+ </UButton>
140
180
  <UButton
141
181
  size="xs"
142
182
  variant="ghost"
@@ -239,7 +279,37 @@ const showHistory = ref(false)
239
279
 
240
280
  <footer class="text-xs text-muted">
241
281
  Sessions live on the backend until you close them or 30 min idle.
242
- History stays in this browser only.
282
+ History stays in this browser only. Ctrl+R to search history.
243
283
  </footer>
284
+
285
+ <UModal v-model:open="searchOpen" :title="`Search history (${history.length})`">
286
+ <template #body>
287
+ <div class="space-y-2">
288
+ <UInput
289
+ v-model="searchQuery"
290
+ placeholder="type to filter…"
291
+ autofocus
292
+ icon="i-lucide-search"
293
+ @keydown.enter="searchResults[0] && pickFromSearch(searchResults[0].cmd)"
294
+ />
295
+ <div class="max-h-80 overflow-y-auto rounded-md border border-default divide-y divide-default">
296
+ <button
297
+ v-for="(entry, i) in searchResults"
298
+ :key="i"
299
+ class="w-full text-left px-3 py-2 text-sm font-mono hover:bg-elevated/40 truncate"
300
+ @click="pickFromSearch(entry.cmd)"
301
+ >
302
+ {{ entry.cmd }}
303
+ </button>
304
+ <div v-if="searchResults.length === 0" class="px-3 py-4 text-muted text-center text-sm">
305
+ No matches
306
+ </div>
307
+ </div>
308
+ <p class="text-xs text-muted">
309
+ Enter sends the top match to the active session.
310
+ </p>
311
+ </div>
312
+ </template>
313
+ </UModal>
244
314
  </div>
245
315
  </template>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "3.69.0",
3
+ "version": "3.70.0",
4
4
  "description": "The Operating System for AI Agent Teams",
5
5
  "type": "module",
6
6
  "bin": {
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "arkaos-core"
3
- version = "3.69.0"
3
+ version = "3.70.0"
4
4
  description = "Core engine for ArkaOS — The Operating System for AI Agent Teams"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -2221,216 +2221,8 @@ def _iso_duration_s(start_iso: str, end_iso: str) -> Optional[int]:
2221
2221
  return None
2222
2222
 
2223
2223
 
2224
- # --- Terminal command runner (PR95a v3.51.0) ---
2225
-
2226
- # Allowlist of commands the dashboard terminal can run. Each entry is
2227
- # {id, label, cmd, description}. Server-side enforcement: any request
2228
- # whose `command_id` isn't in this list is rejected. No shell expansion,
2229
- # no globbing, no pipes — subprocess.run with explicit argv only.
2230
- TERMINAL_ALLOWLIST: list[dict] = [
2231
- {
2232
- "id": "arka-status",
2233
- "label": "ArkaOS status",
2234
- "cmd": ["arkaos", "status"],
2235
- "description": "System status (version, departments, agents, projects).",
2236
- },
2237
- {
2238
- "id": "git-status",
2239
- "label": "git status",
2240
- "cmd": ["git", "status", "--short"],
2241
- "description": "Working tree status in short form.",
2242
- },
2243
- {
2244
- "id": "git-log",
2245
- "label": "git log",
2246
- "cmd_template": ["git", "log", "-{count}", "--oneline"],
2247
- "args": [
2248
- {
2249
- "name": "count",
2250
- "label": "Commits to show",
2251
- "choices": ["5", "10", "20", "50"],
2252
- "default": "10",
2253
- },
2254
- ],
2255
- "description": "Most recent N commits (operator picks N).",
2256
- },
2257
- {
2258
- "id": "npm-version",
2259
- "label": "npm view arkaos version",
2260
- "cmd": ["npm", "view", "arkaos", "version"],
2261
- "description": "Latest published ArkaOS version on npm.",
2262
- },
2263
- {
2264
- "id": "pytest-collect",
2265
- "label": "pytest --collect-only",
2266
- "cmd": ["python3", "-m", "pytest", "--collect-only", "-q", "tests/python/"],
2267
- "description": "List every Python test without running.",
2268
- },
2269
- {
2270
- "id": "ls",
2271
- "label": "ls",
2272
- "cmd": ["ls", "-la"],
2273
- "description": "List files in the project root.",
2274
- },
2275
- {
2276
- "id": "arka-costs",
2277
- "label": "ArkaOS costs",
2278
- "cmd_template": ["python3", "-m", "core.runtime.llm_cost_telemetry_cli", "{period}"],
2279
- "args": [
2280
- {
2281
- "name": "period",
2282
- "label": "Period",
2283
- "choices": ["today", "week", "month", "all"],
2284
- "default": "today",
2285
- },
2286
- ],
2287
- "description": "LLM cost summary for the selected period.",
2288
- },
2289
- ]
2290
-
2291
- _TERMINAL_TIMEOUT_S = 15
2292
- _TERMINAL_MAX_OUTPUT = 20_000 # chars — both stdout and stderr capped
2293
-
2294
-
2295
- @app.get("/api/terminal/commands")
2296
- def terminal_commands():
2297
- """List allowlisted commands the dashboard terminal may run.
2298
-
2299
- PR96b v3.56.0 — includes the `args` schema (label / choices /
2300
- default) when an entry has parameters. The raw cmd / cmd_template
2301
- is NEVER returned — defence in depth.
2302
- """
2303
- out: list[dict] = []
2304
- for c in TERMINAL_ALLOWLIST:
2305
- out.append({
2306
- "id": c["id"],
2307
- "label": c["label"],
2308
- "description": c["description"],
2309
- "args": _safe_args_schema(c.get("args")),
2310
- })
2311
- return {"commands": out, "total": len(out)}
2312
-
2313
-
2314
- def _safe_args_schema(args) -> list[dict]:
2315
- """Sanitised arg schema for the public API — never leaks cmd/template."""
2316
- if not isinstance(args, list):
2317
- return []
2318
- out: list[dict] = []
2319
- for a in args:
2320
- if not isinstance(a, dict):
2321
- continue
2322
- choices = a.get("choices")
2323
- if not isinstance(choices, list) or not choices:
2324
- continue
2325
- out.append({
2326
- "name": str(a.get("name") or ""),
2327
- "label": str(a.get("label") or a.get("name") or ""),
2328
- "choices": [str(x) for x in choices],
2329
- "default": str(a.get("default") or choices[0]),
2330
- })
2331
- return out
2332
-
2333
-
2334
- @app.post("/api/terminal/exec")
2335
- def terminal_exec(body: dict):
2336
- """PR95a v3.51.0 — run an allowlisted command and return the output.
2337
-
2338
- Body: ``{"command_id": "<id>"}``. Unknown ids are rejected.
2339
- Returns ``{stdout, stderr, exit_code, duration_ms, command}``.
2340
- Output is capped at ``_TERMINAL_MAX_OUTPUT`` chars per stream.
2341
- """
2342
- if not isinstance(body, dict):
2343
- return {"error": "body must be an object"}
2344
- cid = (body.get("command_id") or "").strip()
2345
- if not cid:
2346
- return {"error": "command_id is required"}
2347
- entry = next((c for c in TERMINAL_ALLOWLIST if c["id"] == cid), None)
2348
- if entry is None:
2349
- return {"error": f"command '{cid}' is not on the allowlist"}
2350
-
2351
- # PR96b v3.56.0 — resolve template + args, or fall back to fixed cmd.
2352
- if "cmd_template" in entry:
2353
- argv, err = _resolve_cmd_template(entry, body.get("args") or {})
2354
- if err:
2355
- return {"error": err}
2356
- else:
2357
- argv = entry["cmd"]
2358
-
2359
- import time
2360
- started = time.monotonic()
2361
- try:
2362
- result = subprocess.run(
2363
- argv,
2364
- cwd=str(ARKAOS_ROOT),
2365
- capture_output=True,
2366
- text=True,
2367
- timeout=_TERMINAL_TIMEOUT_S,
2368
- shell=False,
2369
- )
2370
- except subprocess.TimeoutExpired:
2371
- return {
2372
- "stdout": "",
2373
- "stderr": f"command timed out after {_TERMINAL_TIMEOUT_S}s",
2374
- "exit_code": -1,
2375
- "duration_ms": int(_TERMINAL_TIMEOUT_S * 1000),
2376
- "command": " ".join(argv),
2377
- }
2378
- except OSError as exc:
2379
- return {
2380
- "stdout": "",
2381
- "stderr": f"command failed to launch: {exc}",
2382
- "exit_code": -1,
2383
- "duration_ms": int((time.monotonic() - started) * 1000),
2384
- "command": " ".join(argv),
2385
- }
2386
- duration_ms = int((time.monotonic() - started) * 1000)
2387
- return {
2388
- "stdout": (result.stdout or "")[:_TERMINAL_MAX_OUTPUT],
2389
- "stderr": (result.stderr or "")[:_TERMINAL_MAX_OUTPUT],
2390
- "exit_code": result.returncode,
2391
- "duration_ms": duration_ms,
2392
- "command": " ".join(argv),
2393
- }
2394
-
2395
-
2396
- def _resolve_cmd_template(entry: dict, supplied: dict) -> tuple[list[str], "str | None"]:
2397
- """Substitute `{name}` placeholders in cmd_template with validated args.
2398
-
2399
- Returns (argv, None) on success, ([], error_msg) on validation failure.
2400
- Anything supplied that isn't in the schema's choices is rejected.
2401
- Unknown arg names are also rejected — no silent passthrough.
2402
- """
2403
- schema = entry.get("args") or []
2404
- if not isinstance(schema, list):
2405
- return [], "command has invalid args schema"
2406
- by_name: dict[str, dict] = {a["name"]: a for a in schema if isinstance(a, dict)}
2407
- chosen: dict[str, str] = {}
2408
- for arg_def in schema:
2409
- name = arg_def["name"]
2410
- s = supplied.get(name)
2411
- if s is None:
2412
- chosen[name] = str(arg_def.get("default") or arg_def["choices"][0])
2413
- continue
2414
- s_str = str(s)
2415
- if s_str not in arg_def["choices"]:
2416
- return [], (
2417
- f"arg '{name}'='{s_str}' is not in the allowed choices "
2418
- f"({', '.join(arg_def['choices'])})"
2419
- )
2420
- chosen[name] = s_str
2421
- for k in supplied:
2422
- if k not in by_name:
2423
- return [], f"unknown arg '{k}'"
2424
- argv: list[str] = []
2425
- for tok in entry["cmd_template"]:
2426
- if not isinstance(tok, str):
2427
- continue
2428
- out_tok = tok
2429
- for name, val in chosen.items():
2430
- out_tok = out_tok.replace(f"{{{name}}}", val)
2431
- argv.append(out_tok)
2432
- return argv, None
2433
-
2224
+ # Allowlist runner (PR95a v3.51.0) removed in PR99d v3.70.0 —
2225
+ # replaced by PTY WebSocket from PR99a/b/c.
2434
2226
 
2435
2227
  @app.put("/api/workflows/{workflow_id}/yaml")
2436
2228
  def workflow_update_yaml(workflow_id: str, body: dict):