maqcli 0.7.0 → 0.8.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.
@@ -353,7 +353,7 @@ async function connectMobile(rl) {
353
353
  async function launchUi(authKey) {
354
354
  // Reuse the daemon; open its landing page. Import lazily to avoid a cycle.
355
355
  const { createDaemon } = await import("../server/daemon.js");
356
- const daemon = createDaemon({ token: authKey, version: "0.7.0" });
356
+ const daemon = createDaemon({ token: authKey, version: "0.8.0" });
357
357
  try {
358
358
  const { host, port } = await daemon.listen();
359
359
  const url = `http://${host}:${port}/`;
package/dist/index.js CHANGED
@@ -46,7 +46,7 @@ import { runOrchestration } from "./core/orchestrator.js";
46
46
  import { performUpdate, installedGlobalVersion } from "./core/update.js";
47
47
  import { checkProtectedPath, scanForInjection, securityLog, securityRules, } from "./core/security.js";
48
48
  import { readFileSync, statSync } from "node:fs";
49
- const VERSION = "0.7.0";
49
+ const VERSION = "0.8.0";
50
50
  async function main(argv) {
51
51
  const [command, ...rest] = argv;
52
52
  switch (command) {
@@ -52,7 +52,7 @@ export function createDaemon(opts = {}) {
52
52
  const host = opts.host ?? process.env.MAQ_HOST ?? "127.0.0.1";
53
53
  const port = opts.port ?? Number(process.env.MAQ_PORT ?? 7717);
54
54
  const token = opts.token ?? process.env.MAQ_TOKEN ?? generateToken();
55
- const version = opts.version ?? "0.7.0";
55
+ const version = opts.version ?? "0.8.0";
56
56
  const corsOrigin = opts.corsOrigin ?? process.env.MAQ_CORS_ORIGIN;
57
57
  const registry = opts.registry ?? new SessionRegistry();
58
58
  const interactive = new InteractiveRegistry();
@@ -1,19 +1,26 @@
1
1
  /**
2
- * webui.ts — the self-contained "god-level" control UI the daemon serves at
3
- * `/app` (and `/`). Zero build step, zero dependencies: one HTML document with
2
+ * webui.ts — the self-contained control surface the daemon serves at `/app`
3
+ * (and `/`). Zero build step, zero dependencies: one HTML document with
4
4
  * inline CSS + vanilla JS. It renders the SAME normalized event stream the
5
5
  * mobile app consumes, so there is no second source of truth.
6
6
  *
7
- * Layout (Cursor-style, megalodon theme deep black + electric blue + white):
8
- * left ────────────┬ center ───────────────────────┬ right (collapsible)
9
- * sessions/agents/ goal input + mode toggle │ preview: session │
10
- * history │ (parallel|loop|safe) + send │ detail / result │
11
- * │ │ phase chips + live SSE console JSON │
12
- * └──────────────────┴────────────────────────────────┴──────────────────────┘
13
- * A feature switcher (top bar) runs detect / doctor / connectivity / models.
7
+ * Design system: dark OLED canvas, two elevation surfaces, one signal accent
8
+ * (teal "alive/running", not the generic AI purple/blue gradient). Type:
9
+ * Space Grotesk for the brand mark + section labels (used sparingly), Inter
10
+ * for UI/body, JetBrains Mono for the console/data surfaces. Signature
11
+ * element: a live depth-of-work TIMELINE phases render as connected nodes
12
+ * that fill in as work progresses, with orchestration rounds (parallel/loop/
13
+ * safe) nesting as a sub-rail beneath the active phase replacing static
14
+ * chips with something that actually shows depth of work over time.
14
15
  *
15
- * Auth: the daemon requires a Bearer token for /v1/* . The page reads it from
16
+ * Layout: a named-grid-area app shell. Sidebar collapses to a drawer under
17
+ * 900px; the preview panel becomes a full-screen sheet under 1200px; the
18
+ * composer sticks to the bottom on narrow viewports. Motion is 150/220ms
19
+ * ease-out and fully disabled under prefers-reduced-motion.
20
+ *
21
+ * Auth: the daemon requires a Bearer token for /v1/*. The page reads it from
16
22
  * ?token=… or a prompt, keeps it in memory only, and streams SSE via fetch +
17
- * ReadableStream (so the token rides in the Authorization header, never the URL).
23
+ * ReadableStream (so the token rides in the Authorization header, never the
24
+ * URL).
18
25
  */
19
26
  export declare function webuiHtml(version: string): string;
@@ -1,188 +1,388 @@
1
1
  /**
2
- * webui.ts — the self-contained "god-level" control UI the daemon serves at
3
- * `/app` (and `/`). Zero build step, zero dependencies: one HTML document with
2
+ * webui.ts — the self-contained control surface the daemon serves at `/app`
3
+ * (and `/`). Zero build step, zero dependencies: one HTML document with
4
4
  * inline CSS + vanilla JS. It renders the SAME normalized event stream the
5
5
  * mobile app consumes, so there is no second source of truth.
6
6
  *
7
- * Layout (Cursor-style, megalodon theme deep black + electric blue + white):
8
- * left ────────────┬ center ───────────────────────┬ right (collapsible)
9
- * sessions/agents/ goal input + mode toggle │ preview: session │
10
- * history │ (parallel|loop|safe) + send │ detail / result │
11
- * │ │ phase chips + live SSE console JSON │
12
- * └──────────────────┴────────────────────────────────┴──────────────────────┘
13
- * A feature switcher (top bar) runs detect / doctor / connectivity / models.
7
+ * Design system: dark OLED canvas, two elevation surfaces, one signal accent
8
+ * (teal "alive/running", not the generic AI purple/blue gradient). Type:
9
+ * Space Grotesk for the brand mark + section labels (used sparingly), Inter
10
+ * for UI/body, JetBrains Mono for the console/data surfaces. Signature
11
+ * element: a live depth-of-work TIMELINE phases render as connected nodes
12
+ * that fill in as work progresses, with orchestration rounds (parallel/loop/
13
+ * safe) nesting as a sub-rail beneath the active phase replacing static
14
+ * chips with something that actually shows depth of work over time.
14
15
  *
15
- * Auth: the daemon requires a Bearer token for /v1/* . The page reads it from
16
+ * Layout: a named-grid-area app shell. Sidebar collapses to a drawer under
17
+ * 900px; the preview panel becomes a full-screen sheet under 1200px; the
18
+ * composer sticks to the bottom on narrow viewports. Motion is 150/220ms
19
+ * ease-out and fully disabled under prefers-reduced-motion.
20
+ *
21
+ * Auth: the daemon requires a Bearer token for /v1/*. The page reads it from
16
22
  * ?token=… or a prompt, keeps it in memory only, and streams SSE via fetch +
17
- * ReadableStream (so the token rides in the Authorization header, never the URL).
23
+ * ReadableStream (so the token rides in the Authorization header, never the
24
+ * URL).
18
25
  */
19
26
  export function webuiHtml(version) {
20
27
  // NB: the client script uses only single/double quotes and string
21
28
  // concatenation — no backticks / ${} — so it lives safely inside this
22
- // template literal.
29
+ // template literal (a backtick here previously broke the TS build twice).
23
30
  return `<!doctype html>
24
31
  <html lang="en"><head>
25
32
  <meta charset="utf-8">
26
33
  <meta name="viewport" content="width=device-width, initial-scale=1">
27
- <title>MAQ · megalodon</title>
34
+ <title>MAQ · control</title>
28
35
  <style>
36
+ @media (prefers-reduced-motion: no-preference){
37
+ :root{ --dur-1:150ms; --dur-2:220ms; --ease:cubic-bezier(.2,.7,.2,1); }
38
+ }
39
+ @media (prefers-reduced-motion: reduce){
40
+ :root{ --dur-1:0ms; --dur-2:0ms; --ease:linear; }
41
+ *{ animation-duration:0ms !important; transition-duration:0ms !important; }
42
+ }
29
43
  :root{
30
- --bg:#04060c; --bg2:#080c16; --panel:#0b1120; --panel2:#0e1526; --edge:#182339;
31
- --ink:#eaf1ff; --mut:#8695b3; --blue:#2f81f7; --blue2:#5aa2ff; --white:#ffffff;
32
- --red:#ff4d5e; --ok:#3ad29f; --warn:#f5b44a;
44
+ /* --- color tokens --- */
45
+ --bg:#05060a; --surface:#0d0f16; --surface-2:#12151f; --border:#1c2130; --border-soft:#161a26;
46
+ --ink:#e8ebf3; --ink-dim:#8890a3; --ink-faint:#565d70;
47
+ --accent:#4fd1c5; --accent-ink:#04211d; --accent-soft:rgba(79,209,197,.14); --accent-line:rgba(79,209,197,.4);
48
+ --warn:#f2b84b; --warn-soft:rgba(242,184,75,.14);
49
+ --danger:#f2555a; --danger-soft:rgba(242,85,90,.14);
50
+ --ok:#34d399; --ok-soft:rgba(52,211,153,.14);
51
+ /* --- type scale --- */
52
+ --fs-11:.6875rem; --fs-12:.75rem; --fs-13:.8125rem; --fs-15:.9375rem; --fs-19:1.1875rem; --fs-24:1.5rem;
53
+ --font-display:'Space Grotesk',ui-sans-serif,system-ui,sans-serif;
54
+ --font-body:'Inter',ui-sans-serif,-apple-system,'Segoe UI',Roboto,sans-serif;
55
+ --font-mono:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
56
+ /* --- spacing --- */
57
+ --sp-1:4px; --sp-2:8px; --sp-3:12px; --sp-4:16px; --sp-5:20px; --sp-6:24px; --sp-8:32px;
58
+ --radius:10px; --radius-sm:7px; --radius-lg:14px;
33
59
  }
34
- *{box-sizing:border-box} html,body{height:100%}
35
- body{margin:0;background:
36
- radial-gradient(1200px 500px at 70% -10%, #0b1c38 0%, transparent 60%),
37
- radial-gradient(900px 500px at 0% 110%, #0a1730 0%, transparent 55%), var(--bg);
38
- color:var(--ink); font:14px/1.5 ui-sans-serif,-apple-system,Segoe UI,Roboto,sans-serif}
39
- .mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
40
- header{display:flex;align-items:center;gap:14px;padding:10px 16px;border-bottom:1px solid var(--edge);
41
- background:linear-gradient(180deg,#0a1224,#070b15)}
42
- .brand{font-weight:800;letter-spacing:3px;font-size:16px}
43
- .brand .fin{color:var(--red)} .brand .m{color:var(--blue2)}
44
- .dot{width:9px;height:9px;border-radius:50%;background:var(--mut);box-shadow:0 0 0 3px #ffffff10}
45
- .dot.on{background:var(--ok);box-shadow:0 0 10px var(--ok)}
60
+ *{box-sizing:border-box}
61
+ html,body{height:100%}
62
+ body{
63
+ margin:0; background:var(--bg); color:var(--ink); font-family:var(--font-body);
64
+ font-size:var(--fs-13); line-height:1.55; -webkit-font-smoothing:antialiased;
65
+ }
66
+ h1,h2,h3,h4{font-family:var(--font-display);font-weight:600;margin:0;letter-spacing:-.01em}
67
+ .mono{font-family:var(--font-mono)}
68
+ a{color:var(--accent)}
69
+ button,input,select,textarea{font-family:inherit;color:inherit}
70
+ :focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:4px}
71
+ .sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
72
+ .eyebrow{font-size:var(--fs-11);text-transform:uppercase;letter-spacing:.09em;color:var(--ink-faint);font-weight:600}
73
+
74
+ /* ---------------- app shell ---------------- */
75
+ #app{
76
+ display:grid;height:100vh;
77
+ grid-template-columns:248px 1fr 0px;
78
+ grid-template-rows:56px 1fr;
79
+ grid-template-areas:"topbar topbar topbar" "sidebar main preview";
80
+ transition:grid-template-columns var(--dur-2) var(--ease);
81
+ }
82
+ #app.preview-open{grid-template-columns:248px 1fr 380px}
83
+ @media (max-width:1200px){
84
+ #app.preview-open{grid-template-columns:248px 1fr}
85
+ #app.preview-open #preview{position:fixed;inset:56px 0 0 0;z-index:50;width:100%}
86
+ }
87
+ @media (max-width:900px){
88
+ #app{grid-template-columns:1fr}
89
+ #app{grid-template-areas:"topbar" "main"}
90
+ #sidebar{position:fixed;inset:56px 0 0 0;z-index:60;width:280px;transform:translateX(-100%);
91
+ transition:transform var(--dur-2) var(--ease)}
92
+ #sidebar.open{transform:translateX(0)}
93
+ #scrim{position:fixed;inset:56px 0 0 0;background:#000a;z-index:55;display:none}
94
+ #scrim.show{display:block}
95
+ #app.preview-open #preview{inset:56px 0 0 0}
96
+ }
97
+
98
+ /* ---------------- topbar ---------------- */
99
+ #topbar{
100
+ grid-area:topbar;display:flex;align-items:center;gap:var(--sp-3);padding:0 var(--sp-4);
101
+ border-bottom:1px solid var(--border);background:var(--surface);
102
+ }
103
+ .menu-btn{display:none}
104
+ @media (max-width:900px){ .menu-btn{display:inline-flex} }
105
+ .brand{display:flex;align-items:center;gap:var(--sp-2)}
106
+ .brand-mark{width:22px;height:22px;border-radius:6px;background:var(--accent-soft);border:1px solid var(--accent-line);
107
+ display:flex;align-items:center;justify-content:center;color:var(--accent);font-family:var(--font-display);
108
+ font-weight:700;font-size:12px}
109
+ .brand h1{font-size:var(--fs-15)}
110
+ .status-dot{width:7px;height:7px;border-radius:50%;background:var(--ink-faint);flex:none}
111
+ .status-dot.on{background:var(--ok);box-shadow:0 0 0 3px var(--ok-soft)}
46
112
  .grow{flex:1}
47
- .switch{display:flex;gap:6px;flex-wrap:wrap}
48
- .switch button, .ghost{background:var(--panel2);color:var(--mut);border:1px solid var(--edge);
49
- border-radius:8px;padding:6px 10px;cursor:pointer;font-size:12px}
50
- .switch button:hover,.ghost:hover{color:var(--ink);border-color:var(--blue)}
51
- main{display:grid;grid-template-columns:250px 1fr 0px;height:calc(100vh - 53px);transition:grid-template-columns .18s}
52
- main.preview{grid-template-columns:250px 1fr 360px}
53
- .col{overflow:auto;min-width:0}
54
- .left{border-right:1px solid var(--edge);background:var(--bg2)}
55
- .right{border-left:1px solid var(--edge);background:var(--bg2)}
56
- .sec{padding:10px 12px;border-bottom:1px solid var(--edge);color:var(--mut);
57
- text-transform:uppercase;font-size:10px;letter-spacing:1.5px}
58
- .item{padding:9px 12px;border-bottom:1px solid #0f1626;cursor:pointer}
59
- .item:hover{background:#0f1830}
60
- .item.active{background:#101c38;box-shadow:inset 3px 0 0 var(--blue)}
61
- .item .t{color:var(--ink);font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
62
- .item .s{color:var(--mut);font-size:11px;margin-top:2px}
63
- .badge{display:inline-block;padding:1px 7px;border-radius:999px;border:1px solid var(--edge);font-size:10px}
64
- .b-running{color:var(--blue2);border-color:var(--blue)} .b-done{color:var(--ok);border-color:var(--ok)}
65
- .b-error,.b-cancelled{color:var(--red);border-color:var(--red)} .b-paused{color:var(--warn);border-color:var(--warn)}
66
- .center{display:flex;flex-direction:column}
67
- .composer{padding:14px 16px;border-bottom:1px solid var(--edge)}
68
- .modes{display:flex;gap:8px;margin-bottom:10px}
69
- .mode{flex:1;background:var(--panel);border:1px solid var(--edge);border-radius:10px;padding:9px 10px;cursor:pointer}
70
- .mode.sel{border-color:var(--blue);background:#0f1c3a;box-shadow:0 0 0 1px var(--blue) inset}
71
- .mode h4{margin:0;font-size:13px;color:var(--ink)} .mode p{margin:2px 0 0;font-size:11px;color:var(--mut)}
72
- .composerRow{display:flex;gap:8px;align-items:flex-end}
73
- textarea{flex:1;resize:vertical;min-height:44px;max-height:160px;background:var(--panel2);color:var(--ink);
74
- border:1px solid var(--edge);border-radius:10px;padding:10px 12px;font:14px/1.4 inherit}
75
- textarea:focus{outline:none;border-color:var(--blue)}
76
- .send{background:linear-gradient(180deg,var(--blue2),var(--blue));color:#00122e;font-weight:700;border:none;
77
- border-radius:10px;padding:11px 16px;cursor:pointer}
78
- .send:disabled{opacity:.5;cursor:not-allowed}
79
- .opts{display:flex;gap:14px;align-items:center;margin-top:8px;color:var(--mut);font-size:12px}
80
- .opts select,.opts input{background:var(--panel2);color:var(--ink);border:1px solid var(--edge);border-radius:7px;padding:4px 7px}
81
- .chips{display:flex;gap:8px;padding:10px 16px;flex-wrap:wrap;border-bottom:1px solid var(--edge)}
82
- .chip{display:flex;align-items:center;gap:6px;padding:5px 11px;border-radius:999px;border:1px solid var(--edge);
83
- color:var(--mut);font-size:12px}
84
- .chip.on{color:var(--blue2);border-color:var(--blue)} .chip.done{color:var(--ok);border-color:var(--ok)}
85
- .console{flex:1;overflow:auto;padding:12px 16px}
86
- .ev{display:flex;gap:9px;padding:2px 0;font-size:12.5px}
87
- .ev .ic{width:14px;text-align:center} .ev .tx{white-space:pre-wrap;word-break:break-word}
88
- .e-blue{color:var(--blue2)} .e-mut{color:var(--mut)} .e-ok{color:var(--ok)} .e-red{color:var(--red)} .e-warn{color:var(--warn)}
89
- .empty{color:var(--mut);text-align:center;padding:40px 20px}
90
- pre{white-space:pre-wrap;word-break:break-word;font-size:12px;color:var(--ink)}
91
- .rhead{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border-bottom:1px solid var(--edge)}
92
- a{color:var(--blue2)}
93
- .overlay{position:fixed;inset:0;background:var(--bg);z-index:60;display:none;flex-direction:column}
113
+ .topbar-nav{display:flex;gap:var(--sp-1);flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none}
114
+ .topbar-nav::-webkit-scrollbar{display:none}
115
+ .tbtn{
116
+ background:transparent;color:var(--ink-dim);border:1px solid transparent;border-radius:var(--radius-sm);
117
+ padding:6px 11px;cursor:pointer;font-size:var(--fs-12);font-weight:500;white-space:nowrap;
118
+ display:inline-flex;align-items:center;gap:6px;transition:background var(--dur-1) var(--ease),color var(--dur-1) var(--ease);
119
+ }
120
+ .tbtn:hover{background:var(--surface-2);color:var(--ink)}
121
+ .tbtn.accent{color:var(--accent)}
122
+ .tbtn svg{width:14px;height:14px;flex:none}
123
+
124
+ /* ---------------- sidebar ---------------- */
125
+ #sidebar{grid-area:sidebar;background:var(--surface);border-right:1px solid var(--border);overflow-y:auto;
126
+ display:flex;flex-direction:column}
127
+ .side-sec{padding:var(--sp-3) var(--sp-4) var(--sp-2)}
128
+ .side-sec-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--sp-1)}
129
+ .count-badge{background:var(--surface-2);border:1px solid var(--border);border-radius:999px;color:var(--ink-dim);
130
+ font-size:10px;padding:1px 7px;font-weight:600}
131
+ .count-badge.alert{background:var(--warn-soft);border-color:var(--warn);color:var(--warn)}
132
+ .list{display:flex;flex-direction:column}
133
+ .row{padding:9px var(--sp-4);border-bottom:1px solid var(--border-soft);cursor:pointer;
134
+ display:flex;flex-direction:column;gap:3px;transition:background var(--dur-1) var(--ease)}
135
+ .row:hover{background:var(--surface-2)}
136
+ .row.active{background:var(--surface-2);box-shadow:inset 3px 0 0 var(--accent)}
137
+ .row-title{font-size:var(--fs-13);color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
138
+ .row-meta{font-size:var(--fs-11);color:var(--ink-faint);display:flex;align-items:center;gap:6px;flex-wrap:wrap}
139
+ .tag{border:1px solid var(--border);border-radius:999px;padding:0 6px;font-size:10px;font-weight:600}
140
+ .tag.running{color:var(--accent);border-color:var(--accent-line)}
141
+ .tag.done{color:var(--ok);border-color:var(--ok)}
142
+ .tag.error,.tag.cancelled{color:var(--danger);border-color:var(--danger)}
143
+ .tag.paused{color:var(--warn);border-color:var(--warn)}
144
+ .empty-state{padding:var(--sp-6) var(--sp-4);text-align:center;color:var(--ink-faint);font-size:var(--fs-12)}
145
+ .empty-state svg{width:28px;height:28px;color:var(--ink-faint);margin-bottom:var(--sp-2);opacity:.6}
146
+ .skeleton{background:linear-gradient(90deg,var(--surface-2) 25%,var(--border) 37%,var(--surface-2) 63%);
147
+ background-size:400% 100%;animation:shimmer 1.4s ease infinite;border-radius:6px;height:13px;margin:4px var(--sp-4)}
148
+ @keyframes shimmer{0%{background-position:100% 0}100%{background-position:0 0}}
149
+ @media (prefers-reduced-motion: reduce){ .skeleton{animation:none;opacity:.5} }
150
+
151
+ /* ---------------- main / composer ---------------- */
152
+ #main{grid-area:main;display:flex;flex-direction:column;min-width:0;background:var(--bg)}
153
+ #composer{padding:var(--sp-4) var(--sp-5);border-bottom:1px solid var(--border)}
154
+ .mode-rail{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--sp-2);margin-bottom:var(--sp-3)}
155
+ @media (max-width:640px){ .mode-rail{grid-template-columns:repeat(2,1fr)} }
156
+ .mode-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
157
+ padding:var(--sp-2) var(--sp-3);cursor:pointer;text-align:left;transition:border-color var(--dur-1) var(--ease),
158
+ background var(--dur-1) var(--ease)}
159
+ .mode-card:hover{border-color:var(--ink-faint)}
160
+ .mode-card.sel{border-color:var(--accent);background:var(--accent-soft)}
161
+ .mode-card h4{font-size:var(--fs-12);color:var(--ink);font-family:var(--font-body);font-weight:600}
162
+ .mode-card p{margin:2px 0 0;font-size:var(--fs-11);color:var(--ink-faint);line-height:1.4}
163
+ .mode-card.sel h4{color:var(--accent)}
164
+ .composer-row{display:flex;gap:var(--sp-2);align-items:flex-end}
165
+ #goal{
166
+ flex:1;resize:vertical;min-height:46px;max-height:180px;background:var(--surface);color:var(--ink);
167
+ border:1px solid var(--border);border-radius:var(--radius);padding:11px 13px;font:var(--fs-13)/1.5 var(--font-body);
168
+ }
169
+ #goal::placeholder{color:var(--ink-faint)}
170
+ #goal:focus{outline:none;border-color:var(--accent)}
171
+ .send-btn{
172
+ background:var(--accent);color:var(--accent-ink);font-weight:700;border:none;border-radius:var(--radius);
173
+ padding:12px 18px;cursor:pointer;font-size:var(--fs-13);display:inline-flex;align-items:center;gap:7px;
174
+ transition:filter var(--dur-1) var(--ease)
175
+ }
176
+ .send-btn:hover{filter:brightness(1.08)}
177
+ .send-btn:disabled{opacity:.45;cursor:not-allowed;filter:none}
178
+ .send-btn svg{width:14px;height:14px}
179
+ .opts-row{display:flex;gap:var(--sp-4);align-items:center;margin-top:var(--sp-3);flex-wrap:wrap}
180
+ .opt{display:flex;align-items:center;gap:6px;color:var(--ink-dim);font-size:var(--fs-12)}
181
+ .opt select,.opt input[type=number]{background:var(--surface);color:var(--ink);border:1px solid var(--border);
182
+ border-radius:6px;padding:3px 7px;font-size:var(--fs-12)}
183
+ .opt input[type=checkbox]{accent-color:var(--accent);width:14px;height:14px}
184
+ .kbd-hint{color:var(--ink-faint);font-size:var(--fs-11);margin-left:auto}
185
+ .kbd-hint kbd{background:var(--surface-2);border:1px solid var(--border);border-radius:4px;padding:1px 5px;
186
+ font-family:var(--font-mono);font-size:10px}
187
+
188
+ /* ---------------- timeline (signature element) ---------------- */
189
+ #timeline{padding:var(--sp-3) var(--sp-5);border-bottom:1px solid var(--border);overflow-x:auto}
190
+ .track{display:flex;align-items:center;min-height:34px}
191
+ .node{display:flex;align-items:center;flex:none}
192
+ .node-dot{width:20px;height:20px;border-radius:50%;border:2px solid var(--border);background:var(--surface);
193
+ display:flex;align-items:center;justify-content:center;flex:none;transition:border-color var(--dur-2) var(--ease),
194
+ background var(--dur-2) var(--ease)}
195
+ .node-dot svg{width:11px;height:11px;opacity:0}
196
+ .node.done .node-dot{border-color:var(--ok);background:var(--ok-soft)}
197
+ .node.done .node-dot svg{opacity:1;color:var(--ok)}
198
+ .node.active .node-dot{border-color:var(--accent);background:var(--accent-soft);
199
+ box-shadow:0 0 0 4px var(--accent-soft)}
200
+ .node.active .node-dot{animation:pulse 1.6s ease-in-out infinite}
201
+ @keyframes pulse{0%,100%{box-shadow:0 0 0 3px var(--accent-soft)}50%{box-shadow:0 0 0 7px transparent}}
202
+ .node-label{font-size:var(--fs-12);color:var(--ink-faint);margin-left:7px;white-space:nowrap}
203
+ .node.done .node-label{color:var(--ok)}
204
+ .node.active .node-label{color:var(--accent);font-weight:600}
205
+ .node-line{width:28px;height:1px;background:var(--border);flex:none;margin:0 4px}
206
+ .node.done + .node-line{background:var(--ok)}
207
+ .sub-rail{display:flex;align-items:center;gap:6px;margin-top:6px;margin-left:26px;flex-wrap:wrap}
208
+ .sub-chip{font-size:10px;color:var(--accent);background:var(--accent-soft);border:1px solid var(--accent-line);
209
+ border-radius:999px;padding:2px 8px;font-weight:600}
210
+
211
+ /* ---------------- console ---------------- */
212
+ #console{flex:1;overflow-y:auto;padding:var(--sp-3) var(--sp-5);display:flex;flex-direction:column;gap:6px}
213
+ .ev-card{display:flex;gap:var(--sp-2);padding:7px 9px;border-radius:8px;border:1px solid transparent;
214
+ font-size:var(--fs-12);animation:fade-in var(--dur-2) var(--ease)}
215
+ @keyframes fade-in{from{opacity:0;transform:translateY(2px)}to{opacity:1;transform:translateY(0)}}
216
+ @media (prefers-reduced-motion: reduce){ .ev-card{animation:none} }
217
+ .ev-ic{width:16px;height:16px;flex:none;margin-top:1px}
218
+ .ev-ic svg{width:100%;height:100%}
219
+ .ev-body{flex:1;min-width:0}
220
+ .ev-text{white-space:pre-wrap;word-break:break-word;font-family:var(--font-mono);font-size:11.5px;line-height:1.5}
221
+ .ev-time{color:var(--ink-faint);font-size:10px;margin-left:7px}
222
+ .c-accent{color:var(--accent)} .c-dim{color:var(--ink-faint)} .c-ok{color:var(--ok)}
223
+ .c-danger{color:var(--danger)} .c-warn{color:var(--warn)}
224
+ .ev-card.bg-danger{background:var(--danger-soft)} .ev-card.bg-warn{background:var(--warn-soft)}
225
+
226
+ /* ---------------- preview panel ---------------- */
227
+ #preview{grid-area:preview;background:var(--surface);border-left:1px solid var(--border);overflow-y:auto;min-width:0}
228
+ .panel-head{display:flex;align-items:center;justify-content:space-between;padding:var(--sp-3) var(--sp-4);
229
+ border-bottom:1px solid var(--border);position:sticky;top:0;background:var(--surface);z-index:2}
230
+ .panel-head h3{font-size:var(--fs-13)}
231
+ .icon-btn{background:transparent;border:1px solid var(--border);border-radius:7px;color:var(--ink-dim);
232
+ cursor:pointer;padding:5px 9px;font-size:var(--fs-12);display:inline-flex;align-items:center;gap:5px}
233
+ .icon-btn:hover{color:var(--ink);border-color:var(--ink-faint)}
234
+
235
+ pre.data{white-space:pre-wrap;word-break:break-word;font-family:var(--font-mono);font-size:11.5px;color:var(--ink);
236
+ padding:var(--sp-4);margin:0;line-height:1.6}
237
+
238
+ /* ---------------- full-screen overlays ---------------- */
239
+ .overlay{position:fixed;inset:0;background:var(--bg);z-index:100;display:none;flex-direction:column}
94
240
  .overlay.show{display:flex}
95
- .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:12px;padding:16px;overflow:auto;align-content:start}
96
- .fcard{background:var(--panel);border:1px solid var(--edge);border-radius:12px;padding:14px;cursor:pointer}
97
- .fcard:hover{border-color:var(--blue);box-shadow:0 0 0 1px var(--blue) inset}
98
- .fcard h4{margin:0 0 4px;color:var(--ink);font-size:14px}
99
- .fcard .cat{font-size:10px;color:var(--blue2);text-transform:uppercase;letter-spacing:1px}
100
- .fcard p{margin:6px 0 0;font-size:12px;color:var(--mut)}
101
- .req{padding:9px 12px;border-bottom:1px solid #0f1626}
102
- .req .a{font-size:12.5px;color:var(--ink)} .req .r{font-size:11px;color:var(--mut);margin:2px 0 6px}
103
- .req.destructive{box-shadow:inset 3px 0 0 var(--red)} .req.major{box-shadow:inset 3px 0 0 var(--warn)}
104
- .req .btns{display:flex;gap:6px}
105
- .req button{border:none;border-radius:6px;padding:4px 10px;cursor:pointer;font-size:11px}
106
- .ok-btn{background:var(--ok);color:#00231a} .no-btn{background:var(--red);color:#2a0007}
107
- .sechead{display:flex;align-items:center;gap:8px;margin:18px 0 8px}
108
- .sechead:first-child{margin-top:0}
109
- .sechead .n{background:var(--panel2);border:1px solid var(--edge);border-radius:999px;padding:2px 9px;font-size:11px;color:var(--mut)}
110
- .seclist{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:6px}
111
- .secitem{background:var(--panel);border:1px solid var(--edge);border-radius:8px;padding:8px 10px;font-family:ui-monospace,monospace;font-size:12px;color:var(--ink);word-break:break-all}
112
- .secnote{color:var(--mut);font-size:12px;margin:4px 0 14px}
113
- .pill2{display:inline-flex;align-items:center;gap:6px;background:var(--panel2);border:1px solid var(--edge);border-radius:999px;padding:5px 12px;font-size:12px;color:var(--ink);margin-right:8px}
114
- .pill2.on{color:var(--ok);border-color:var(--ok)}
241
+ .overlay-head{display:flex;align-items:center;gap:var(--sp-3);padding:var(--sp-3) var(--sp-5);
242
+ border-bottom:1px solid var(--border);background:var(--surface)}
243
+ .back-btn{background:var(--surface-2);border:1px solid var(--border);color:var(--ink);border-radius:var(--radius-sm);
244
+ padding:7px 12px;cursor:pointer;font-size:var(--fs-12);display:inline-flex;align-items:center;gap:6px;font-weight:500}
245
+ .back-btn:hover{border-color:var(--accent);color:var(--accent)}
246
+ .overlay-body{flex:1;overflow-y:auto;padding:var(--sp-5)}
247
+ .fgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:var(--sp-3)}
248
+ .fcard{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:var(--sp-4);
249
+ cursor:pointer;transition:border-color var(--dur-1) var(--ease),transform var(--dur-1) var(--ease);text-align:left}
250
+ .fcard:hover{border-color:var(--accent-line);transform:translateY(-1px)}
251
+ .fcard .eyebrow{color:var(--accent)}
252
+ .fcard h4{font-size:var(--fs-13);margin:var(--sp-1) 0 4px;font-family:var(--font-body)}
253
+ .fcard p{margin:0;font-size:var(--fs-12);color:var(--ink-dim);line-height:1.5}
254
+
255
+ .req-card{padding:10px var(--sp-4);border-bottom:1px solid var(--border-soft);border-left:3px solid transparent}
256
+ .req-card.destructive{border-left-color:var(--danger);background:var(--danger-soft)}
257
+ .req-card.major{border-left-color:var(--warn);background:var(--warn-soft)}
258
+ .req-action{font-size:var(--fs-12);color:var(--ink)}
259
+ .req-reason{font-size:var(--fs-11);color:var(--ink-faint);margin:3px 0 8px}
260
+ .req-btns{display:flex;gap:6px}
261
+ .btn-sm{border:none;border-radius:6px;padding:5px 12px;cursor:pointer;font-size:11px;font-weight:600}
262
+ .btn-approve{background:var(--ok);color:#04231a}
263
+ .btn-deny{background:var(--danger);color:#2e0507}
264
+
265
+ .sec-block{margin-bottom:var(--sp-6)}
266
+ .sec-head{display:flex;align-items:center;gap:var(--sp-2);margin-bottom:var(--sp-2)}
267
+ .sec-head h4{font-size:var(--fs-13);font-family:var(--font-body)}
268
+ .sec-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:6px;margin-bottom:var(--sp-2)}
269
+ .sec-item{background:var(--surface-2);border:1px solid var(--border);border-radius:7px;padding:7px 10px;
270
+ font-family:var(--font-mono);font-size:11.5px;color:var(--ink);word-break:break-all}
271
+ .sec-note{color:var(--ink-faint);font-size:var(--fs-11);line-height:1.5}
272
+ .posture-row{display:flex;gap:var(--sp-2);margin-bottom:var(--sp-5);flex-wrap:wrap}
273
+ .posture-pill{display:inline-flex;align-items:center;gap:6px;background:var(--ok-soft);border:1px solid var(--ok);
274
+ color:var(--ok);border-radius:999px;padding:5px 12px;font-size:var(--fs-12);font-weight:600}
275
+ .posture-pill svg{width:12px;height:12px}
115
276
  </style></head>
116
277
  <body>
117
- <header>
118
- <span class="dot" id="dot"></span>
119
- <span class="brand"><span class="m">M A</span> <span class="fin">Q</span> &#9650;</span>
120
- <span class="e-mut" style="font-size:12px">megalodon · v${version}</span>
121
- <span class="grow"></span>
122
- <div class="switch">
123
- <button onclick="openFeatures()">&#9776; Features</button>
124
- <button onclick="feat('agents')">Agents</button>
125
- <button onclick="feat('connectivity')">Connectivity</button>
126
- <button onclick="feat('doctor')">Doctor</button>
127
- <button onclick="feat('models')">Models</button>
128
- <button onclick="openSecurity()">&#128274; Security</button>
129
- <button class="ghost" onclick="togglePreview()">Preview</button>
130
- </div>
131
- </header>
132
- <main id="main">
133
- <div class="col left">
134
- <div class="sec">Request box <span id="reqCount" class="badge"></span></div>
135
- <div id="requests"><div class="empty">No pending requests.</div></div>
136
- <div class="sec">Sessions &amp; history</div>
137
- <div id="sessions"><div class="empty">No sessions yet.</div></div>
138
- <div class="sec">Agents</div>
139
- <div id="agents"><div class="empty">—</div></div>
140
- </div>
141
- <div class="col center">
142
- <div class="composer">
143
- <div class="modes" id="modes"></div>
144
- <div class="composerRow">
278
+ <div id="app">
279
+ <header id="topbar">
280
+ <button class="menu-btn icon-btn" id="menuBtn" aria-label="Open sidebar" onclick="toggleSidebar()">
281
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
282
+ </button>
283
+ <div class="brand">
284
+ <span class="brand-mark" aria-hidden="true">M</span>
285
+ <h1>MAQ</h1>
286
+ <span class="status-dot" id="statusDot" role="status" aria-label="Disconnected"></span>
287
+ </div>
288
+ <span class="grow"></span>
289
+ <nav class="topbar-nav" aria-label="Tools">
290
+ <button class="tbtn accent" onclick="openFeatures()">
291
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
292
+ Features
293
+ </button>
294
+ <button class="tbtn" onclick="feat('agents')">Agents</button>
295
+ <button class="tbtn" onclick="feat('connectivity')">Connectivity</button>
296
+ <button class="tbtn" onclick="feat('doctor')">Doctor</button>
297
+ <button class="tbtn" onclick="feat('models')">Models</button>
298
+ <button class="tbtn" onclick="openSecurity()">
299
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2 4 5v6c0 5 3.5 8.5 8 11 4.5-2.5 8-6 8-11V5l-8-3z"/></svg>
300
+ Security
301
+ </button>
302
+ <button class="tbtn" id="previewToggle" onclick="togglePreview()">Preview</button>
303
+ </nav>
304
+ </header>
305
+
306
+ <div id="scrim" onclick="toggleSidebar()"></div>
307
+
308
+ <aside id="sidebar" aria-label="Sessions and agents">
309
+ <div class="side-sec">
310
+ <div class="side-sec-head">
311
+ <span class="eyebrow">Request box</span>
312
+ <span class="count-badge" id="reqCount">0</span>
313
+ </div>
314
+ <div class="list" id="requests" role="region" aria-label="Pending permission requests"></div>
315
+ </div>
316
+ <div class="side-sec">
317
+ <div class="side-sec-head"><span class="eyebrow">Sessions</span></div>
318
+ <div class="list" id="sessions" role="region" aria-label="Session history"></div>
319
+ </div>
320
+ <div class="side-sec">
321
+ <div class="side-sec-head"><span class="eyebrow">Agents</span></div>
322
+ <div class="list" id="agents" role="region" aria-label="Detected worker agents"></div>
323
+ </div>
324
+ </aside>
325
+
326
+ <main id="main">
327
+ <div id="composer">
328
+ <div class="mode-rail" id="modes" role="radiogroup" aria-label="Execution mode"></div>
329
+ <div class="composer-row">
330
+ <label class="sr-only" for="goal">Goal</label>
145
331
  <textarea id="goal" placeholder="Describe a goal — e.g. add pagination to the users endpoint and add tests"></textarea>
146
- <button class="send" id="send" onclick="startTask()">Send &#9654;</button>
332
+ <button class="send-btn" id="send" onclick="startTask()">
333
+ Send
334
+ <svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 12l18-9-6 9 6 9-18-9z"/></svg>
335
+ </button>
147
336
  </div>
148
- <div class="opts">
149
- <label>target
337
+ <div class="opts-row">
338
+ <label class="opt">target
150
339
  <select id="target">
151
340
  <option value="none">none (raw)</option><option value="auto">auto</option>
152
341
  <option value="claude-code">claude-code</option><option value="codex">codex</option>
153
342
  <option value="gemini">gemini</option>
154
343
  </select>
155
344
  </label>
156
- <label><input type="checkbox" id="dry"> dry-run</label>
157
- <label>concurrency <input type="number" id="conc" value="4" min="1" max="16" style="width:52px"></label>
345
+ <label class="opt"><input type="checkbox" id="dry"> dry-run</label>
346
+ <label class="opt">concurrency <input type="number" id="conc" value="4" min="1" max="16" style="width:52px"></label>
347
+ <span class="kbd-hint"><kbd>Ctrl</kbd>+<kbd>Enter</kbd> to send</span>
158
348
  </div>
159
349
  </div>
160
- <div class="chips" id="chips"></div>
161
- <div class="console" id="console"><div class="empty">Pick a mode, type a goal, and press Send. The live Scout&#8594;Plan&#8594;Execute&#8594;Verify stream shows here.</div></div>
162
- </div>
163
- <div class="col right">
164
- <div class="rhead"><b>Preview</b><button class="ghost" onclick="togglePreview()">Close</button></div>
165
- <div id="preview" style="padding:12px"><div class="empty">Select a session to inspect its result.</div></div>
166
- </div>
167
- </main>
168
- <div class="overlay" id="features">
169
- <div class="rhead" style="padding:14px 16px">
170
- <button class="ghost" onclick="closeFeatures()">&#8592; Back to home</button>
171
- <b style="margin-left:12px">All features</b>
350
+ <div id="timeline" aria-label="Progress timeline"><div class="track" id="track"></div><div id="subrail"></div></div>
351
+ <div id="console" role="log" aria-live="polite" aria-label="Live activity"></div>
352
+ </main>
353
+
354
+ <aside id="preview" aria-label="Preview panel">
355
+ <div class="panel-head"><h3>Preview</h3><button class="icon-btn" onclick="togglePreview()">Close</button></div>
356
+ <div id="previewBody"></div>
357
+ </aside>
358
+ </div>
359
+
360
+ <div class="overlay" id="features" role="dialog" aria-label="All features">
361
+ <div class="overlay-head">
362
+ <button class="back-btn" onclick="closeFeatures()">
363
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
364
+ Back to home
365
+ </button>
366
+ <h3 style="margin-left:4px">All features</h3>
172
367
  <span class="grow"></span>
173
- <span class="e-mut" style="font-size:12px">everything MAQ can do — click to run</span>
368
+ <span class="eyebrow" style="text-transform:none;letter-spacing:0">everything MAQ can do — click to run</span>
174
369
  </div>
175
- <div class="grid" id="featureGrid"><div class="empty">loading…</div></div>
370
+ <div class="overlay-body"><div class="fgrid" id="featureGrid"></div></div>
176
371
  </div>
177
- <div class="overlay" id="security">
178
- <div class="rhead" style="padding:14px 16px">
179
- <button class="ghost" onclick="closeSecurity()">&#8592; Back to home</button>
180
- <b style="margin-left:12px">Security rules</b>
372
+
373
+ <div class="overlay" id="security" role="dialog" aria-label="Security rules">
374
+ <div class="overlay-head">
375
+ <button class="back-btn" onclick="closeSecurity()">
376
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
377
+ Back to home
378
+ </button>
379
+ <h3 style="margin-left:4px">Security rules</h3>
181
380
  <span class="grow"></span>
182
- <span class="e-mut" style="font-size:12px">enforced at the code level — same rules the terminal ('maq security') shows and enforces</span>
381
+ <span class="eyebrow" style="text-transform:none;letter-spacing:0">enforced at the code level — same rules the terminal ('maq security') shows and enforces</span>
183
382
  </div>
184
- <div id="securityBody" style="overflow:auto;padding:16px"><div class="empty">loading…</div></div>
383
+ <div class="overlay-body" id="securityBody"></div>
185
384
  </div>
385
+
186
386
  <script>
187
387
  "use strict";
188
388
  var base = location.origin;
@@ -190,6 +390,20 @@ var token = new URLSearchParams(location.search).get('token') || sessionStorage.
190
390
  if(!token){ token = prompt('Daemon auth token (printed by: maq serve — or the launcher):') || ''; }
191
391
  sessionStorage.setItem('maqToken', token);
192
392
 
393
+ var ICONS = {
394
+ play:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>',
395
+ chevron:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>',
396
+ dot:'<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="4"/></svg>',
397
+ check:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 13l4 4L19 7"/></svg>',
398
+ warn:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 9v4M12 17h.01M10.3 3.9L2.7 17a2 2 0 0 0 1.7 3h15.2a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/></svg>',
399
+ x:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg>',
400
+ tool:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>',
401
+ flag:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 22V4M4 4h11l-1.5 4L15 12H4"/></svg>',
402
+ verified:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l3 1.5 3.3-.3 1 3.1 2.7 2-1.3 3 1.3 3-2.7 2-1 3.1-3.3-.3L12 22l-3-1.5-3.3.3-1-3.1-2.7-2 1.3-3-1.3-3 2.7-2 1-3.1 3.3.3z"/><path d="M9 12l2 2 4-4"/></svg>',
403
+ circle:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/></svg>',
404
+ empty:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M3 7l3-4h12l3 4"/></svg>'
405
+ };
406
+
193
407
  var MODES = [
194
408
  {id:'', h:'Single', p:'one Scout&#8594;Plan&#8594;Execute&#8594;Verify pass'},
195
409
  {id:'parallel',h:'Parallel', p:'split, run at once, join &amp; repeat'},
@@ -198,72 +412,117 @@ var MODES = [
198
412
  ];
199
413
  var mode = 'parallel';
200
414
  var activeId = null;
415
+ var connected = false;
201
416
 
202
- function h(tag,cls,txt){ var e=document.createElement(tag); if(cls)e.className=cls; if(txt!=null)e.textContent=txt; return e; }
417
+ function h(tag,cls,html){ var e=document.createElement(tag); if(cls)e.className=cls; if(html!=null)e.innerHTML=html; return e; }
203
418
  function headers(){ return {authorization:'Bearer '+token,'content-type':'application/json'}; }
204
419
  function api(path,opts){ opts=opts||{}; opts.headers=headers(); return fetch(base+path,opts); }
420
+ function setConnected(v){
421
+ connected=v;
422
+ var dot=document.getElementById('statusDot');
423
+ dot.className='status-dot'+(v?' on':'');
424
+ dot.setAttribute('aria-label', v?'Connected':'Disconnected');
425
+ }
426
+ function emptyState(icon,title,hint){
427
+ return '<div class="empty-state">'+(ICONS[icon]||ICONS.circle)+'<div>'+title+'</div>'+
428
+ (hint?'<div style="margin-top:2px;opacity:.8">'+hint+'</div>':'')+'</div>';
429
+ }
430
+ function skeletons(n){
431
+ var s=''; for(var i=0;i<n;i++)s+='<div class="skeleton" style="width:'+(60+((i*17)%35))+'%"></div>';
432
+ return s;
433
+ }
434
+
435
+ function toggleSidebar(){
436
+ document.getElementById('sidebar').classList.toggle('open');
437
+ document.getElementById('scrim').classList.toggle('show');
438
+ }
205
439
 
206
440
  function renderModes(){
207
441
  var c=document.getElementById('modes'); c.innerHTML='';
208
442
  MODES.forEach(function(m){
209
- var d=h('div','mode'+(m.id===mode?' sel':''));
210
- d.innerHTML='<h4>'+m.h+'</h4><p>'+m.p+'</p>';
443
+ var d=h('button','mode-card'+(m.id===mode?' sel':''),'<h4>'+m.h+'</h4><p>'+m.p+'</p>');
444
+ d.setAttribute('role','radio'); d.setAttribute('aria-checked', m.id===mode?'true':'false');
211
445
  d.onclick=function(){ mode=m.id; renderModes(); };
212
446
  c.appendChild(d);
213
447
  });
214
448
  }
215
449
 
216
- var PHASES=['scout','plan','execute','verify'];
217
- function renderChips(events){
218
- var done={}, active=null, orch=[];
450
+ /* ---------------- signature element: depth-of-work timeline ---------------- */
451
+ var PHASES=[
452
+ {id:'scout',label:'Scout'},{id:'plan',label:'Plan'},{id:'execute',label:'Execute'},{id:'verify',label:'Verify'}
453
+ ];
454
+ function renderTimeline(events){
455
+ var done={}, active=null, orchRounds=[];
219
456
  events.forEach(function(e){
220
457
  var p=e.data&&e.data.phase;
221
458
  if(!p)return;
222
- if(/^(parallel-round|loop-iteration|safe-)/.test(p)){ if(e.type==='phase.started')orch.push(p); return; }
459
+ if(/^(parallel-round|loop-iteration|safe-)/.test(p)){
460
+ if(e.type==='phase.started' && orchRounds.indexOf(p)===-1) orchRounds.push(p);
461
+ return;
462
+ }
223
463
  if(e.type==='phase.started')active=p;
224
- if(e.type==='phase.done'){done[p]=true; if(active===p)active=null;}
464
+ if(e.type==='phase.done'){ done[p]=true; if(active===p)active=null; }
225
465
  });
226
- var c=document.getElementById('chips'); c.innerHTML='';
227
- PHASES.forEach(function(p){
228
- var cls='chip'+(done[p]?' done':(active===p?' on':''));
229
- var chip=h('div',cls); chip.innerHTML=(done[p]?'&#10003; ':(active===p?'&#9679; ':'&#9675; '))+p; c.appendChild(chip);
466
+ var track=document.getElementById('track'); track.innerHTML='';
467
+ PHASES.forEach(function(ph,i){
468
+ var state = done[ph.id] ? 'done' : (active===ph.id ? 'active' : '');
469
+ var node=h('div','node'+(state?' '+state:''));
470
+ node.innerHTML='<span class="node-dot">'+ICONS.check+'</span><span class="node-label">'+ph.label+'</span>';
471
+ track.appendChild(node);
472
+ if(i<PHASES.length-1) track.appendChild(h('div','node-line'));
230
473
  });
231
- var last=orch[orch.length-1];
232
- if(last){ var oc=h('div','chip on'); oc.textContent='&#9670; '+last; oc.innerHTML='&#9670; '+last; c.appendChild(oc); }
474
+ var sub=document.getElementById('subrail');
475
+ if(orchRounds.length){
476
+ sub.innerHTML='<div class="sub-rail">'+orchRounds.map(function(r){return '<span class="sub-chip">'+r+'</span>';}).join('')+'</div>';
477
+ } else { sub.innerHTML=''; }
478
+ }
479
+
480
+ function fmtTime(ts){
481
+ try{ var d=new Date(ts); return d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'}); }catch(e){ return ''; }
233
482
  }
234
483
 
235
- function iconFor(e){
484
+ function renderEvent(e){
236
485
  switch(e.type){
237
- case 'task.started': return ['&#9654;','e-blue', (e.data.goal||e.data.task||'')+(e.data.mode?' ['+e.data.mode+']':'')];
238
- case 'phase.started': return ['&#8250;','e-blue', (e.data.phase||'')+' …'];
239
- case 'phase.done': return ['&#8226;','e-mut', (e.data.phase||'')+(e.data.reason?' '+e.data.reason:'')];
240
- case 'agent.event': return ['&#183;','e-mut', e.data.note||JSON.stringify(e.data)];
241
- case 'agent.stdout': return ['|','e-mut', e.data.text||''];
242
- case 'agent.stderr': return ['!','e-warn', e.data.text||''];
243
- case 'tool.call': return ['&#9881;','e-blue','tool: '+(e.data.tool||JSON.stringify(e.data.command||e.data.raw||{}))];
244
- case 'task.done': return [e.data.verified?'&#10003;':'&#9873;', e.data.verified?'e-ok':'e-warn', 'done · verified='+e.data.verified+(e.data.mode?' · '+e.data.mode:'')+(e.data.summary?' · '+e.data.summary:'')];
245
- case 'task.error': return ['&#10007;','e-red','error: '+(e.data.message||'')];
246
- case 'task.cancelled': return ['&#10007;','e-red','cancelled'];
247
- default: return ['&#9675;','e-mut', e.type];
486
+ case 'task.started': return [ICONS.play,'c-accent',(e.data.goal||e.data.task||'')+(e.data.mode?' ['+e.data.mode+']':''),false];
487
+ case 'phase.started': return [ICONS.chevron,'c-accent',(e.data.phase||'')+' …',false];
488
+ case 'phase.done': return [ICONS.dot,'c-dim',(e.data.phase||'')+(e.data.reason?' '+e.data.reason:''),false];
489
+ case 'agent.event': return [ICONS.dot,'c-dim',e.data.note||JSON.stringify(e.data),false];
490
+ case 'agent.stdout': return [ICONS.dot,'c-dim',e.data.text||'',false];
491
+ case 'agent.stderr': return [ICONS.warn,'c-warn',e.data.text||'',true];
492
+ case 'tool.call': return [ICONS.tool,'c-accent','tool: '+(e.data.tool||JSON.stringify(e.data.command||e.data.raw||{})),false];
493
+ case 'task.done': return [e.data.verified?ICONS.verified:ICONS.flag, e.data.verified?'c-ok':'c-warn',
494
+ 'done · verified='+e.data.verified+(e.data.mode?' · '+e.data.mode:'')+(e.data.summary?' · '+e.data.summary:''), !e.data.verified];
495
+ case 'task.error': return [ICONS.x,'c-danger','error: '+(e.data.message||''),true];
496
+ case 'task.cancelled': return [ICONS.x,'c-danger','cancelled',true];
497
+ default: return [ICONS.circle,'c-dim',e.type,false];
248
498
  }
249
499
  }
250
500
  function pushEvent(e){
251
501
  var c=document.getElementById('console');
252
- if(c.firstChild&&c.firstChild.className==='empty')c.innerHTML='';
253
- var parts=iconFor(e);
254
- var row=h('div','ev');
255
- var ic=h('span','ic '+parts[1]); ic.innerHTML=parts[0];
256
- var tx=h('span','tx '+parts[1]); tx.textContent=parts[2];
257
- row.appendChild(ic); row.appendChild(tx); c.appendChild(row);
502
+ var first=c.firstElementChild;
503
+ if(first && first.classList && first.classList.contains('empty-state-wrap')) c.innerHTML='';
504
+ var parts=renderEvent(e);
505
+ var card=h('div','ev-card'+(parts[3]?(parts[1]==='c-danger'?' bg-danger':' bg-warn'):''));
506
+ card.innerHTML='<span class="ev-ic '+parts[1]+'">'+parts[0]+'</span>'+
507
+ '<span class="ev-body"><span class="ev-text '+parts[1]+'"></span><span class="ev-time">'+fmtTime(e.ts)+'</span></span>';
508
+ card.querySelector('.ev-text').textContent = parts[2];
509
+ c.appendChild(card);
258
510
  c.scrollTop=c.scrollHeight;
259
511
  }
512
+ function clearConsole(){
513
+ document.getElementById('console').innerHTML =
514
+ '<div class="empty-state-wrap">'+emptyState('empty','Pick a mode, describe a goal, and press Send.',
515
+ 'The live Scout&#8594;Plan&#8594;Execute&#8594;Verify stream renders here.')+'</div>';
516
+ }
260
517
 
261
518
  var evList=[];
262
519
  async function streamEvents(id){
263
520
  evList=[];
521
+ clearConsole();
264
522
  document.getElementById('console').innerHTML='';
523
+ renderTimeline([]);
265
524
  var res=await fetch(base+'/v1/sessions/'+id+'/events',{headers:{authorization:'Bearer '+token}});
266
- if(!res.ok||!res.body){ pushEvent({type:'task.error',data:{message:'stream failed '+res.status}}); return; }
525
+ if(!res.ok||!res.body){ pushEvent({type:'task.error',ts:new Date().toISOString(),data:{message:'stream failed '+res.status}}); return; }
267
526
  var reader=res.body.getReader(), dec=new TextDecoder(), buf='';
268
527
  while(true){
269
528
  var r=await reader.read(); if(r.done)break;
@@ -275,7 +534,7 @@ async function streamEvents(id){
275
534
  var ln=lines[j];
276
535
  if(ln.indexOf('data:')===0){
277
536
  var payload=ln.slice(5).trim(); if(!payload)continue;
278
- try{ var ev=JSON.parse(payload); if(ev.type==='stream.end'){continue;} evList.push(ev); pushEvent(ev); renderChips(evList); }catch(err){}
537
+ try{ var ev=JSON.parse(payload); if(ev.type==='stream.end'){continue;} evList.push(ev); pushEvent(ev); renderTimeline(evList); }catch(err){}
279
538
  }
280
539
  }
281
540
  }
@@ -285,7 +544,8 @@ async function streamEvents(id){
285
544
  }
286
545
 
287
546
  async function startTask(){
288
- var goal=document.getElementById('goal').value.trim(); if(!goal)return;
547
+ var goalEl=document.getElementById('goal');
548
+ var goal=goalEl.value.trim(); if(!goal)return;
289
549
  var btn=document.getElementById('send'); btn.disabled=true;
290
550
  var body={task:goal, target:document.getElementById('target').value, dryRun:document.getElementById('dry').checked,
291
551
  maxConcurrency:Number(document.getElementById('conc').value)||4};
@@ -293,131 +553,143 @@ async function startTask(){
293
553
  try{
294
554
  var res=await api('/v1/sessions',{method:'POST',body:JSON.stringify(body)});
295
555
  var s=await res.json();
296
- if(!res.ok){ pushEvent({type:'task.error',data:{message:s.error||('HTTP '+res.status)}}); return; }
297
- activeId=s.id; document.getElementById('goal').value='';
556
+ if(!res.ok){ pushEvent({type:'task.error',ts:new Date().toISOString(),data:{message:s.error||('HTTP '+res.status)}}); return; }
557
+ activeId=s.id; goalEl.value='';
298
558
  await refreshSessions();
299
559
  streamEvents(s.id);
300
- }catch(e){ pushEvent({type:'task.error',data:{message:String(e)}}); }
560
+ }catch(e){ pushEvent({type:'task.error',ts:new Date().toISOString(),data:{message:String(e)}}); }
301
561
  finally{ btn.disabled=false; }
302
562
  }
303
563
 
564
+ function sessionRow(s){
565
+ var it=h('div','row'+(s.id===activeId?' active':''));
566
+ it.setAttribute('tabindex','0'); it.setAttribute('role','button');
567
+ it.setAttribute('aria-label','Session: '+s.task);
568
+ var verifiedMark = s.verified===true?' · '+ICONS.check.replace('<svg','<svg style=\\'width:10px;height:10px;vertical-align:-1px\\''):(s.verified===false?' · unverified':'');
569
+ it.innerHTML =
570
+ '<div class="row-title">'+escText(s.task)+'</div>'+
571
+ '<div class="row-meta"><span class="tag '+s.status+'">'+s.status+'</span>'+
572
+ (s.mode?'<span>'+s.mode+'</span>':'')+'<span>'+(s.target||'auto')+'</span><span>'+s.eventCount+' ev</span>'+verifiedMark+'</div>';
573
+ it.onclick=function(){ activeId=s.id; refreshSessions(); openPreview(s.id); streamEvents(s.id); if(window.innerWidth<=900)toggleSidebar(); };
574
+ it.onkeydown=function(ev){ if(ev.key==='Enter')it.click(); };
575
+ return it;
576
+ }
577
+ function escText(s){ var d=document.createElement('div'); d.textContent=String(s==null?'':s); return d.innerHTML; }
578
+
579
+ var sessionsLoaded=false;
304
580
  async function refreshSessions(){
581
+ var box=document.getElementById('sessions');
582
+ if(!sessionsLoaded) box.innerHTML = skeletons(3);
305
583
  try{
306
584
  var res=await api('/v1/sessions'); var d=await res.json();
307
- var box=document.getElementById('sessions'); box.innerHTML='';
585
+ setConnected(true);
308
586
  var list=(d.sessions||[]);
309
- document.getElementById('dot').className='dot on';
310
- if(!list.length){ box.innerHTML='<div class="empty">No sessions yet.</div>'; return; }
311
- list.slice().reverse().forEach(function(s){
312
- var it=h('div','item'+(s.id===activeId?' active':''));
313
- var t=h('div','t'); t.textContent=s.task;
314
- var meta=h('div','s');
315
- meta.innerHTML='<span class="badge b-'+s.status+'">'+s.status+'</span> '+
316
- (s.mode?('· '+s.mode+' '):'')+'· '+(s.target||'auto')+' · '+s.eventCount+' ev'+
317
- (s.verified===true?' · &#10003;':(s.verified===false?' · &#10007;':''));
318
- it.appendChild(t); it.appendChild(meta);
319
- it.onclick=function(){ activeId=s.id; refreshSessions(); openPreview(s.id); streamEvents(s.id); };
320
- box.appendChild(it);
321
- });
322
- }catch(e){ document.getElementById('dot').className='dot'; }
587
+ sessionsLoaded=true;
588
+ box.innerHTML='';
589
+ if(!list.length){ box.innerHTML=emptyState('empty','No sessions yet','Send a goal to start one.'); return; }
590
+ list.slice().reverse().forEach(function(s){ box.appendChild(sessionRow(s)); });
591
+ }catch(e){ setConnected(false); if(!sessionsLoaded) box.innerHTML=emptyState('warn','Can\\'t reach the daemon',''); }
323
592
  }
324
593
 
325
594
  async function openPreview(id){
326
- document.getElementById('main').classList.add('preview');
327
- var box=document.getElementById('preview'); box.innerHTML='loading…';
595
+ document.getElementById('app').classList.add('preview-open');
596
+ var box=document.getElementById('previewBody'); box.innerHTML=skeletons(4);
328
597
  try{
329
598
  var res=await api('/v1/sessions/'+id); var d=await res.json();
330
- box.innerHTML='';
331
- var pre=h('pre'); pre.textContent=JSON.stringify({id:d.id,task:d.task,status:d.status,mode:d.mode,verified:d.verified,error:d.error},null,2);
332
- box.appendChild(pre);
333
- }catch(e){ box.textContent=String(e); }
599
+ var pre=h('pre','data');
600
+ pre.textContent=JSON.stringify({id:d.id,task:d.task,status:d.status,mode:d.mode,verified:d.verified,error:d.error},null,2);
601
+ box.innerHTML=''; box.appendChild(pre);
602
+ }catch(e){ box.innerHTML=emptyState('warn','Could not load session',String(e)); }
334
603
  }
335
- function togglePreview(){ document.getElementById('main').classList.toggle('preview'); }
604
+ function togglePreview(){ document.getElementById('app').classList.toggle('preview-open'); }
336
605
 
337
606
  async function feat(kind){
338
- document.getElementById('main').classList.add('preview');
339
- var box=document.getElementById('preview'); box.innerHTML='running '+kind+'…';
607
+ document.getElementById('app').classList.add('preview-open');
608
+ var box=document.getElementById('previewBody'); box.innerHTML=skeletons(5);
340
609
  try{
341
610
  var out;
342
611
  if(kind==='agents'){ out=await (await api('/v1/agents')).json(); renderAgents(out.agents||[]); }
343
612
  else if(kind==='connectivity'){ out=await (await api('/v1/connectivity')).json(); }
344
613
  else { out=await (await api('/v1/exec',{method:'POST',body:JSON.stringify({argv:[kind]})})).json(); }
345
- var pre=h('pre'); pre.textContent=(out.stdout!=null?out.stdout:'')+(out.stderr?('\\n'+out.stderr):'')||JSON.stringify(out,null,2);
614
+ var pre=h('pre','data');
615
+ pre.textContent=(out.stdout!=null?out.stdout:'')+(out.stderr?('\\n'+out.stderr):'')||JSON.stringify(out,null,2);
346
616
  box.innerHTML=''; box.appendChild(pre);
347
- }catch(e){ box.textContent=String(e); }
617
+ }catch(e){ box.innerHTML=emptyState('warn','Could not run '+kind,String(e)); }
348
618
  }
349
619
  function renderAgents(agents){
350
- var box=document.getElementById('agents'); box.innerHTML='';
351
- if(!agents.length){ box.innerHTML='<div class="empty">none detected</div>'; return; }
620
+ var box=document.getElementById('agents');
621
+ if(!agents.length){ box.innerHTML=emptyState('empty','No agents detected','Install/log in to an AI CLI.'); return; }
622
+ box.innerHTML='';
352
623
  agents.forEach(function(a){
353
- var it=h('div','item'); var t=h('div','t'); t.textContent=a.name;
354
- var s=h('div','s'); s.textContent=a.installed?(a.authenticated?'authenticated':'installed'):'not found';
355
- it.appendChild(t); it.appendChild(s); box.appendChild(it);
624
+ var it=h('div','row');
625
+ var statusText = a.installed?(a.authenticated?'authenticated':'installed, logged out'):'not found';
626
+ var tagCls = a.installed?(a.authenticated?'done':'paused'):'cancelled';
627
+ it.innerHTML='<div class="row-title">'+escText(a.name)+'</div><div class="row-meta"><span class="tag '+tagCls+'">'+statusText+'</span></div>';
628
+ box.appendChild(it);
356
629
  });
357
630
  }
358
631
 
359
632
  async function openFeatures(){
360
633
  document.getElementById('features').classList.add('show');
361
- var grid=document.getElementById('featureGrid'); grid.innerHTML='loading…';
634
+ var grid=document.getElementById('featureGrid'); grid.innerHTML=skeletons(6);
362
635
  try{
363
636
  var cat=await (await api('/v1/commands')).json();
364
637
  var cmds=(cat.maq||[]); grid.innerHTML='';
638
+ if(!cmds.length){ grid.innerHTML=emptyState('empty','No commands available',''); return; }
365
639
  cmds.forEach(function(c){
366
- var card=h('div','fcard');
367
- card.innerHTML='<div class="cat">'+c.category+'</div><h4>'+c.name+'</h4><p>'+c.summary+'</p>';
640
+ var card=h('button','fcard');
641
+ card.innerHTML='<div class="eyebrow">'+escText(c.category)+'</div><h4>'+escText(c.name)+'</h4><p>'+escText(c.summary)+'</p>';
368
642
  card.onclick=function(){ runFeature(c); };
369
643
  grid.appendChild(card);
370
644
  });
371
- }catch(e){ grid.innerHTML='<div class="empty">'+e+'</div>'; }
645
+ }catch(e){ grid.innerHTML=emptyState('warn','Could not load features',String(e)); }
372
646
  }
373
647
  function closeFeatures(){ document.getElementById('features').classList.remove('show'); }
374
648
 
375
649
  async function openSecurity(){
376
650
  document.getElementById('security').classList.add('show');
377
- var box=document.getElementById('securityBody'); box.innerHTML='loading…';
651
+ var box=document.getElementById('securityBody'); box.innerHTML=skeletons(8);
378
652
  try{
379
653
  var d=await (await api('/v1/security')).json();
380
654
  var r=d.rules, evs=(d.events||[]);
381
655
  var html='';
382
- html+='<div style="margin-bottom:14px">';
383
- html+='<span class="pill2 on">permission posture: '+r.permissionMode+'</span>';
384
- html+='<span class="pill2 on">secret env scrubbing: on</span>';
385
- html+='<span class="pill2 on">prompt-injection scanning: on</span>';
656
+ html+='<div class="posture-row">';
657
+ html+='<span class="posture-pill">'+ICONS.check+' permission posture: '+r.permissionMode+'</span>';
658
+ html+='<span class="posture-pill">'+ICONS.check+' secret env scrubbing: on</span>';
659
+ html+='<span class="posture-pill">'+ICONS.check+' prompt-injection scanning: on</span>';
386
660
  html+='</div>';
387
661
 
388
- html+=sec('Protected paths (never writable — cannot be overridden by config)', r.protectedPaths.length);
389
- html+='<div class="seclist">'+r.protectedPaths.map(function(p){return '<div class="secitem">'+esc(p)+'</div>';}).join('')+'</div>';
390
- html+='<div class="secnote">Enforced in sandbox.ts checkWrite(), unconditionally, before any tier logic a compromised agent cannot argue past this with a clever prompt.</div>';
391
-
392
- html+=sec('Protected file patterns (matched by name, anywhere in the project)', r.protectedNamePatterns.length);
393
- html+='<div class="seclist">'+r.protectedNamePatterns.map(function(p){return '<div class="secitem">'+esc(p)+'</div>';}).join('')+'</div>';
394
-
662
+ html+=secBlock('Protected paths', 'never writable — cannot be overridden by config', r.protectedPaths,
663
+ 'Enforced in sandbox.ts checkWrite(), unconditionally, before any tier logic — a compromised agent cannot argue past this with a clever prompt.');
664
+ html+=secBlock('Protected file patterns', 'matched by name, anywhere in the project', r.protectedNamePatterns, '');
395
665
  if(r.extraProtectedPaths && r.extraProtectedPaths.length){
396
- html+=sec('Project-added protected paths', r.extraProtectedPaths.length);
397
- html+='<div class="seclist">'+r.extraProtectedPaths.map(function(p){return '<div class="secitem">'+esc(p)+'</div>';}).join('')+'</div>';
666
+ html+=secBlock('Project-added protected paths', '', r.extraProtectedPaths, '');
398
667
  }
668
+ html+=secBlock('Network egress allowlist', 'default-deny everything else', r.netAllowlist,
669
+ 'Enforced in tools.ts http_get and available to any outbound call site via security.ts checkEgress(). Not just a suggestion in a prompt.');
670
+ html+=secBlock('Prompt-injection scanning', '', r.promptInjectionScanning,
671
+ 'README, manifest, git commits, AGENTS.md and skill files are scanned for override/exfiltration/jailbreak patterns before they reach a model. Findings are surfaced loudly, never silently dropped.');
399
672
 
400
- html+=sec('Network egress allowlist (default-deny everything else)', r.netAllowlist.length);
401
- html+='<div class="seclist">'+r.netAllowlist.map(function(p){return '<div class="secitem">'+esc(p)+'</div>';}).join('')+'</div>';
402
- html+='<div class="secnote">Enforced in tools.ts http_get and available to any outbound call site via security.ts checkEgress(). Not just a suggestion in a prompt.</div>';
403
-
404
- html+=sec('Prompt-injection scanning', r.promptInjectionScanning.length);
405
- html+='<div class="seclist">'+r.promptInjectionScanning.map(function(p){return '<div class="secitem">'+esc(p)+'</div>';}).join('')+'</div>';
406
- html+='<div class="secnote">README, manifest, git commits, AGENTS.md and skill files are scanned for override/exfiltration/jailbreak patterns before they reach a model. Findings are surfaced loudly, never silently dropped.</div>';
407
-
408
- html+=sec('Recent findings (this session)', evs.length);
409
- if(!evs.length){ html+='<div class="secnote">None recorded yet — findings appear here as tasks run.</div>'; }
673
+ html+='<div class="sec-block"><div class="sec-head"><h4>Recent findings</h4><span class="count-badge'+(evs.length?' alert':'')+'">'+evs.length+'</span></div>';
674
+ if(!evs.length){ html+='<div class="sec-note">None recorded yet — findings appear here as tasks run.</div>'; }
410
675
  else {
411
676
  html+='<div>'+evs.slice().reverse().map(function(e){
412
- var cls=e.severity==='high'?'e-red':'e-warn';
413
- return '<div class="ev"><span class="ic '+cls+'">&#9888;</span><span class="tx '+cls+'">'+esc(e.kind)+': '+esc(e.detail)+'</span></div>';
677
+ var cls=e.severity==='high'?'c-danger':'c-warn';
678
+ return '<div class="ev-card"><span class="ev-ic '+cls+'">'+ICONS.warn+'</span><span class="ev-text '+cls+'">'+escText(e.kind)+': '+escText(e.detail)+'</span></div>';
414
679
  }).join('')+'</div>';
415
680
  }
416
- }catch(e){ box.innerHTML='<div class="empty">'+e+'</div>'; return; }
417
- box.innerHTML = html;
681
+ html+='</div>';
682
+ box.innerHTML = html;
683
+ }catch(e){ box.innerHTML=emptyState('warn','Could not load security rules',String(e)); }
684
+ }
685
+ function secBlock(title,note,items,footer){
686
+ var html='<div class="sec-block"><div class="sec-head"><h4>'+title+'</h4><span class="count-badge">'+(items?items.length:0)+'</span>'+
687
+ (note?'<span class="sec-note">'+note+'</span>':'')+'</div>';
688
+ html+='<div class="sec-list">'+(items||[]).map(function(p){return '<div class="sec-item">'+escText(p)+'</div>';}).join('')+'</div>';
689
+ if(footer) html+='<div class="sec-note">'+footer+'</div>';
690
+ html+='</div>';
691
+ return html;
418
692
  }
419
- function sec(title,count){ return '<div class="sechead"><b>'+title+'</b><span class="n">'+count+'</span></div>'; }
420
- function esc(s){ var d=document.createElement('div'); d.textContent=String(s); return d.innerHTML; }
421
693
  function closeSecurity(){ document.getElementById('security').classList.remove('show'); }
422
694
 
423
695
  function runFeature(c){
@@ -435,18 +707,19 @@ async function loadRequests(){
435
707
  try{
436
708
  var d=await (await api('/v1/requests')).json();
437
709
  var box=document.getElementById('requests'); var pend=(d.pending||[]);
438
- document.getElementById('reqCount').textContent=pend.length?String(pend.length):'';
439
- if(!pend.length){ box.innerHTML='<div class="empty">No pending requests.</div>'; return; }
710
+ var badge=document.getElementById('reqCount');
711
+ badge.textContent=String(pend.length);
712
+ badge.className='count-badge'+(pend.length?' alert':'');
713
+ if(!pend.length){ box.innerHTML=emptyState('empty','No pending requests',''); return; }
440
714
  box.innerHTML='';
441
715
  pend.forEach(function(r){
442
- var it=h('div','req '+r.risk);
443
- var a=h('div','a'); a.textContent=r.action+' — '+(r.detail||'');
444
- var reason=h('div','r'); reason.textContent=r.risk+' · '+(r.reason||'');
445
- var btns=h('div','btns');
446
- var ok=h('button','ok-btn','Approve'); ok.onclick=function(){ decideReq(r.id,'approve'); };
447
- var no=h('button','no-btn','Deny'); no.onclick=function(){ decideReq(r.id,'deny'); };
448
- btns.appendChild(ok); btns.appendChild(no);
449
- it.appendChild(a); it.appendChild(reason); it.appendChild(btns); box.appendChild(it);
716
+ var it=h('div','req-card '+r.risk);
717
+ it.innerHTML='<div class="req-action">'+escText(r.action)+' — '+escText(r.detail||'')+'</div>'+
718
+ '<div class="req-reason">'+escText(r.risk)+' · '+escText(r.reason||'')+'</div>'+
719
+ '<div class="req-btns"><button class="btn-sm btn-approve">Approve</button><button class="btn-sm btn-deny">Deny</button></div>';
720
+ it.querySelector('.btn-approve').onclick=function(){ decideReq(r.id,'approve'); };
721
+ it.querySelector('.btn-deny').onclick=function(){ decideReq(r.id,'deny'); };
722
+ box.appendChild(it);
450
723
  });
451
724
  }catch(e){}
452
725
  }
@@ -454,9 +727,11 @@ async function decideReq(id,action){
454
727
  try{ await api('/v1/requests/'+id,{method:'POST',body:JSON.stringify({action:action})}); loadRequests(); }catch(e){}
455
728
  }
456
729
 
457
- renderModes(); refreshSessions(); feat('agents'); loadRequests();
730
+ clearConsole();
731
+ renderModes(); renderTimeline([]); refreshSessions(); feat('agents'); loadRequests();
458
732
  setInterval(refreshSessions, 5000); setInterval(loadRequests, 3000);
459
733
  document.getElementById('goal').addEventListener('keydown', function(e){ if((e.metaKey||e.ctrlKey)&&e.key==='Enter')startTask(); });
734
+ document.addEventListener('keydown', function(e){ if(e.key==='Escape'){ closeFeatures(); closeSecurity(); } });
460
735
  </script>
461
736
  </body></html>`;
462
737
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maqcli",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "MAQ master orchestrator — a token-efficient, agent-agnostic supervisor CLI that sits on top of any worker CLI (AI or not) via a Scout -> Plan -> Execute -> Verify pipeline.",
5
5
  "type": "module",
6
6
  "bin": {