@monster-orange/den 0.4.11 → 0.4.12

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.
@@ -138,7 +138,7 @@ function rowHtml(r) {
138
138
  ? ` <span class="tbl-muted">· ${esc(r.model)}</span>`
139
139
  : "";
140
140
  return `<tr class="agent" data-agent="${esc(r.agent)}" data-state="${esc(r.state)}">
141
- <th scope="row"><span class="repo-name">${esc(r.agent)}</span>${persona}</th>
141
+ <th scope="row">${r.displayName ? `<span class="repo-name">${esc(r.displayName)}</span> <span class="tbl-muted">${esc(r.agent)}</span>` : `<span class="repo-name">${esc(r.agent)}</span>`}${persona}</th>
142
142
  <td class="tbl-mono">${esc(r.runtime ?? "—")}${model}</td>
143
143
  <td>${stateCell(r.state)}</td>
144
144
  <td class="tbl-mono tbl-muted agent-cwd" title="${esc(r.cwd ?? "")}">${esc(r.cwd ?? "—")}</td>
@@ -50,13 +50,26 @@
50
50
  flex-direction: column;
51
51
  overflow: hidden;
52
52
  }
53
+ /* AG-4b: the main area is a column that fills the window between
54
+ the top bar and the runs dock (the body is the viewport's height,
55
+ the shell's column puts the dock at its foot); the list and the
56
+ panes take what is left, 14px above the dock. No 100vh guess. */
57
+ html body.den-has-shell {
58
+ height: 100%;
59
+ min-height: 0;
60
+ }
53
61
  html body.den-has-shell > #den-content {
54
62
  overflow: auto;
55
63
  min-width: 0;
56
- padding: 20px 24px 32px 24px;
64
+ min-height: 0;
65
+ padding: 20px 24px 14px 24px;
57
66
  font: 14px/1.45 var(--sans);
58
67
  color: var(--text);
59
- display: block;
68
+ display: flex;
69
+ flex-direction: column;
70
+ }
71
+ #den-content > .ph {
72
+ flex: none;
60
73
  }
61
74
  #den-content :focus-visible {
62
75
  outline: 2px solid var(--accent);
@@ -94,9 +107,38 @@
94
107
  .agx {
95
108
  display: grid;
96
109
  grid-template-columns: 300px minmax(0, 1fr);
110
+ grid-template-rows: minmax(0, 1fr);
97
111
  gap: 14px;
98
- min-height: 420px;
99
- height: calc(100vh - 300px);
112
+ flex: 1;
113
+ min-height: 0;
114
+ }
115
+ /* The layout switch (AG-4b), the prototype's .seg. */
116
+ .seg {
117
+ display: inline-flex;
118
+ border: 1px solid var(--border);
119
+ border-radius: var(--radius-button);
120
+ overflow: hidden;
121
+ flex-wrap: wrap;
122
+ }
123
+ .seg button {
124
+ background: none;
125
+ border: 0;
126
+ padding: 6px 12px;
127
+ cursor: pointer;
128
+ color: var(--muted);
129
+ font: inherit;
130
+ }
131
+ .seg button[aria-pressed="true"] {
132
+ background: color-mix(in srgb, var(--accent) 16%, transparent);
133
+ color: var(--text);
134
+ font-weight: 500;
135
+ }
136
+ .agl .aghint {
137
+ padding: 10px 12px;
138
+ font-size: 12px;
139
+ color: var(--faint);
140
+ border-top: 1px solid var(--border);
141
+ margin-top: 6px;
100
142
  }
101
143
  .agl {
102
144
  border: 1px solid var(--border);
@@ -245,6 +287,98 @@
245
287
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
246
288
  grid-auto-rows: minmax(0, 1fr);
247
289
  }
290
+ .agp.tabs {
291
+ grid-template-columns: minmax(0, 1fr);
292
+ grid-template-rows: auto minmax(0, 1fr);
293
+ gap: 0;
294
+ }
295
+ .agp > .pane[hidden] {
296
+ display: none;
297
+ }
298
+ .agp.tabs > .pane {
299
+ border-top-left-radius: 0;
300
+ }
301
+ /* The tab strip (the prototype's .agtabs): name, persona · workspace. */
302
+ .agtabs {
303
+ display: flex;
304
+ gap: 2px;
305
+ border-bottom: 1px solid var(--border);
306
+ overflow-x: auto;
307
+ min-width: 0;
308
+ }
309
+ .agtab-w {
310
+ position: relative;
311
+ display: inline-flex;
312
+ flex: none;
313
+ max-width: 280px;
314
+ }
315
+ .agtab {
316
+ display: inline-flex;
317
+ align-items: center;
318
+ gap: 6px;
319
+ min-width: 0;
320
+ border: 1px solid transparent;
321
+ border-bottom: 0;
322
+ background: none;
323
+ padding: 6px 28px 6px 10px;
324
+ border-radius: var(--radius-button) var(--radius-button) 0 0;
325
+ cursor: pointer;
326
+ color: var(--muted);
327
+ white-space: nowrap;
328
+ font: 13px var(--sans);
329
+ }
330
+ .agtab[hidden] {
331
+ display: none;
332
+ }
333
+ .agtab[aria-selected="true"] {
334
+ background: var(--surface);
335
+ border-color: var(--border);
336
+ color: var(--text);
337
+ margin-bottom: -1px;
338
+ }
339
+ .agtab .den-dot {
340
+ margin: 0;
341
+ }
342
+ .agtab .tn {
343
+ overflow: hidden;
344
+ text-overflow: ellipsis;
345
+ }
346
+ .agtab .tm {
347
+ color: var(--faint);
348
+ font-size: 11px;
349
+ overflow: hidden;
350
+ text-overflow: ellipsis;
351
+ }
352
+ .agtab-x {
353
+ position: absolute;
354
+ top: 0;
355
+ bottom: 0;
356
+ right: 4px;
357
+ display: flex;
358
+ align-items: center;
359
+ padding: 0 4px;
360
+ border: 0;
361
+ background: none;
362
+ cursor: pointer;
363
+ font: 13px var(--sans);
364
+ color: var(--faint);
365
+ }
366
+ .agtab-x:hover {
367
+ color: var(--bad);
368
+ }
369
+ .agtab-in {
370
+ font: 13px var(--sans);
371
+ width: 200px;
372
+ margin: 3px 28px 3px 6px;
373
+ padding: 2px 6px;
374
+ border: 1px solid var(--accent);
375
+ border-radius: var(--radius-button);
376
+ background: var(--bg);
377
+ color: var(--text);
378
+ }
379
+ .den-sr[hidden] {
380
+ display: none;
381
+ }
248
382
  .pane {
249
383
  display: grid;
250
384
  grid-template-rows: auto minmax(0, 1fr);
@@ -292,6 +426,9 @@
292
426
  gap: 6px;
293
427
  flex: none;
294
428
  }
429
+ .pane .bar2 .x [hidden] {
430
+ display: none;
431
+ }
295
432
  /* The body: mono 12px/1.5 (xterm draws inside it). */
296
433
  .pane .term {
297
434
  min-height: 0;
@@ -301,6 +438,15 @@
301
438
  color: var(--text);
302
439
  overflow: hidden;
303
440
  }
441
+ .pane .term {
442
+ display: grid;
443
+ grid-template: minmax(0, 1fr) / minmax(0, 1fr);
444
+ }
445
+ .pane .term .tbox {
446
+ min-height: 0;
447
+ min-width: 0;
448
+ overflow: hidden;
449
+ }
304
450
  .pane .term .xterm {
305
451
  height: 100%;
306
452
  }
@@ -430,7 +576,8 @@
430
576
  @media (max-width: 860px) {
431
577
  .agx {
432
578
  grid-template-columns: minmax(0, 1fr);
433
- height: auto;
579
+ grid-template-rows: none;
580
+ flex: none;
434
581
  }
435
582
  .ph .act {
436
583
  flex-wrap: wrap;
@@ -465,6 +612,32 @@
465
612
  </div>
466
613
  </div>
467
614
  <div class="act">
615
+ <div class="seg" role="group" aria-label="Layout" data-layouts>
616
+ <button
617
+ type="button"
618
+ data-layout="focus"
619
+ aria-pressed="true"
620
+ aria-keyshortcuts="Control+Shift+1"
621
+ >
622
+ Focus
623
+ </button>
624
+ <button
625
+ type="button"
626
+ data-layout="tabs"
627
+ aria-pressed="false"
628
+ aria-keyshortcuts="Control+Shift+2"
629
+ >
630
+ Tabs
631
+ </button>
632
+ <button
633
+ type="button"
634
+ data-layout="tile"
635
+ aria-pressed="false"
636
+ aria-keyshortcuts="Control+Shift+3"
637
+ >
638
+ Tile
639
+ </button>
640
+ </div>
468
641
  <button type="button" class="den-btn" id="ag-clear" hidden>
469
642
  Clear stopped
470
643
  </button>
@@ -877,6 +877,8 @@ function followScopeInUrl(value) {
877
877
  async function _switchScope(value, focus, inView) {
878
878
  if (value !== "all" && value !== focus && !(await _setFocus(value))) return;
879
879
  if (inView) {
880
+ // A ?ws= page shows the pick after the reload, not its old ?ws=.
881
+ followScopeInUrl(value);
880
882
  location.reload();
881
883
  return;
882
884
  }
@@ -327,6 +327,7 @@
327
327
  "set.language": "Idioma",
328
328
  "set.appearance.org-theme": "Tema predeterminado de la org",
329
329
  "set.editor": "IDE predeterminado",
330
+ "set.editor.open": "Abrir enlaces de archivo en",
330
331
  "set.editor_overrides": "IDE por lenguaje",
331
332
  "set.terminal": "Terminal predeterminada",
332
333
  "set.shell.pages": "Páginas",
@@ -341,6 +342,7 @@
341
342
  "set.graph.panelWidth": "Panel de detalles",
342
343
  "set.workspace.editor": "IDE de este espacio de trabajo",
343
344
  "set.agents.runtimes": "Runtimes",
345
+ "set.agents.layout": "Disposición de la pestaña Agentes",
344
346
  "set.cli.local_dev": "Entorno local",
345
347
  "set.cli.cli_tabs": "Pestañas de terminal",
346
348
  "set.cli.log_window_lines": "Líneas del panel de registros",
@@ -475,6 +477,23 @@
475
477
  "ag.palStart": "Iniciar agente…",
476
478
  "ag.cap":
477
479
  "Hasta 4 paneles: se cerró el de {id} para hacer sitio (sigue en marcha)",
480
+ "ag.layout": "Disposición",
481
+ "ag.l.focus": "Enfoque",
482
+ "ag.l.tabs": "Pestañas",
483
+ "ag.l.tile": "Mosaico",
484
+ "ag.l.tip": "{name} ({key})",
485
+ "ag.hint.keys":
486
+ "⌃⇧1 / ⌃⇧2 / ⌃⇧3 cambian la disposición. ⌘ y clic (Ctrl y clic) abre un enlace o una ruta.",
487
+ "ag.pal.layout": "Disposición: {name}",
488
+ "ag.copied": "Copiado",
489
+ "ag.tabs": "Sesiones abiertas",
490
+ "ag.hint.focus":
491
+ "Un clic muestra una sesión. Mayús o ⌘ y clic la añade al lado.",
492
+ "ag.hint.tabs":
493
+ "Un clic abre una sesión en una pestaña. Doble clic en una pestaña para cambiarle el nombre.",
494
+ "ag.hint.tile": "Un clic añade una sesión al mosaico, hasta 4.",
495
+ "ag.renameTip": "Doble clic para cambiar el nombre",
496
+ "ag.renameName": "Cambiar el nombre de {id}",
478
497
  "ag.rt.ready": "Inicia {name} {version} desde",
479
498
  "ag.rt.readyNoVer": "Inicia {name} desde",
480
499
  "ag.rt.notFound": "No se encontró {name}.",
@@ -316,6 +316,7 @@
316
316
  "set.language": "言語",
317
317
  "set.appearance.org-theme": "組織の既定テーマ",
318
318
  "set.editor": "既定の IDE",
319
+ "set.editor.open": "ファイルリンクを開くエディター",
319
320
  "set.editor_overrides": "言語ごとの IDE",
320
321
  "set.terminal": "既定のターミナル",
321
322
  "set.shell.pages": "ページ",
@@ -330,6 +331,7 @@
330
331
  "set.graph.panelWidth": "詳細パネル",
331
332
  "set.workspace.editor": "このワークスペースの IDE",
332
333
  "set.agents.runtimes": "ランタイム",
334
+ "set.agents.layout": "エージェントタブのレイアウト",
333
335
  "set.cli.local_dev": "ローカル環境",
334
336
  "set.cli.cli_tabs": "ターミナルのタブ",
335
337
  "set.cli.log_window_lines": "ログ欄の行数",
@@ -455,6 +457,23 @@
455
457
  "ag.palStart": "エージェントを開始…",
456
458
  "ag.cap":
457
459
  "ペインは最大 4 つです。{id} のペインを閉じました(実行は続きます)",
460
+ "ag.layout": "レイアウト",
461
+ "ag.l.focus": "フォーカス",
462
+ "ag.l.tabs": "タブ",
463
+ "ag.l.tile": "タイル",
464
+ "ag.l.tip": "{name}({key})",
465
+ "ag.hint.keys":
466
+ "⌃⇧1 / ⌃⇧2 / ⌃⇧3 でレイアウトを切り替えます。⌘ クリック(Ctrl クリック)でリンクやパスを開きます。",
467
+ "ag.pal.layout": "レイアウト: {name}",
468
+ "ag.copied": "コピーしました",
469
+ "ag.tabs": "開いているセッション",
470
+ "ag.hint.focus":
471
+ "クリックで 1 つのセッションを表示します。Shift か ⌘ を押しながらクリックすると横に追加します。",
472
+ "ag.hint.tabs":
473
+ "クリックでセッションをタブで開きます。タブをダブルクリックすると名前を変えられます。",
474
+ "ag.hint.tile": "クリックでセッションをタイルに追加します(最大 4)。",
475
+ "ag.renameTip": "ダブルクリックで名前を変更",
476
+ "ag.renameName": "{id} の名前を変更",
458
477
  "ag.rt.ready": "{name} {version} を起動:",
459
478
  "ag.rt.readyNoVer": "{name} を起動:",
460
479
  "ag.rt.notFound": "{name} が見つかりません。",
@@ -628,6 +628,33 @@ export const DEN_SETTINGS = [
628
628
  read: (ctx) => effective(ctx.data.editors, ctx.data.editor),
629
629
  write: (v, ctx) => ctx.post({ editor: v || null }),
630
630
  },
631
+ {
632
+ id: "editor.open",
633
+ category: "editor",
634
+ label: "Open file links in",
635
+ description:
636
+ "⌘-click (Ctrl-click on Windows and Linux) on a file path in an agent's pane opens it here, at the line. None copies the path instead.",
637
+ keywords: [
638
+ "editor",
639
+ "link",
640
+ "path",
641
+ "file",
642
+ "agents",
643
+ "vscode",
644
+ "cursor",
645
+ "zed",
646
+ ],
647
+ scope: "user",
648
+ control: seg([
649
+ { value: "vscode", label: "VS Code" },
650
+ { value: "cursor", label: "Cursor" },
651
+ { value: "zed", label: "Zed" },
652
+ { value: "none", label: "None (copy)" },
653
+ ]),
654
+ keys: ["editor_open"],
655
+ read: (ctx) => ctx.data.editor_open ?? "none",
656
+ write: (v, ctx) => ctx.post({ editor_open: v }),
657
+ },
631
658
  {
632
659
  id: "editor_overrides",
633
660
  category: "editor",
@@ -813,6 +840,23 @@ export const DEN_SETTINGS = [
813
840
  },
814
841
 
815
842
  // ── Agents ─────────────────────────────────────────────────────────────
843
+ {
844
+ id: "agents.layout",
845
+ category: "agents",
846
+ label: "Agents tab layout",
847
+ description:
848
+ "Focus shows one agent, Tabs shows one with a tab for each, Tile shows up to four in a grid. The Agents tab's switch, ⌃⇧1, ⌃⇧2, ⌃⇧3 and ⌘K set it too.",
849
+ keywords: ["agents", "layout", "focus", "tabs", "tile", "grid", "panes"],
850
+ scope: "user",
851
+ control: seg([
852
+ { value: "focus", label: "Focus" },
853
+ { value: "tabs", label: "Tabs" },
854
+ { value: "tile", label: "Tile" },
855
+ ]),
856
+ keys: ["agents_layout"],
857
+ read: (ctx) => ctx.data.agents_layout ?? "focus",
858
+ write: (v, ctx) => ctx.post({ agents_layout: v }),
859
+ },
816
860
  {
817
861
  id: "agents.runtimes",
818
862
  category: "agents",
@@ -438,6 +438,7 @@
438
438
  "set.language": "Language",
439
439
  "set.appearance.org-theme": "Org default theme",
440
440
  "set.editor": "Default IDE",
441
+ "set.editor.open": "Open file links in",
441
442
  "set.editor_overrides": "Per-language IDE",
442
443
  "set.terminal": "Default terminal",
443
444
  "set.shell.pages": "Pages",
@@ -452,6 +453,7 @@
452
453
  "set.graph.panelWidth": "Details panel",
453
454
  "set.workspace.editor": "IDE for this workspace",
454
455
  "set.agents.runtimes": "Runtimes",
456
+ "set.agents.layout": "Agents tab layout",
455
457
  "set.cli.local_dev": "Local environment",
456
458
  "set.cli.cli_tabs": "Terminal tabs",
457
459
  "set.cli.log_window_lines": "Log pane lines",
@@ -585,6 +587,24 @@
585
587
  "ag.pal": "Agents › {ws} / {name}",
586
588
  "ag.palStart": "Start agent…",
587
589
  "ag.cap": "Up to 4 panes: {id}'s closed to make room (it keeps running)",
590
+ // AG-4b: the Agents tab's layouts and named tabs.
591
+ "ag.layout": "Layout",
592
+ "ag.l.focus": "Focus",
593
+ "ag.l.tabs": "Tabs",
594
+ "ag.l.tile": "Tile",
595
+ "ag.l.tip": "{name} ({key})",
596
+ "ag.hint.keys":
597
+ "⌃⇧1 / ⌃⇧2 / ⌃⇧3 switch layout. ⌘-click (Ctrl-click) opens a link or path.",
598
+ "ag.pal.layout": "Layout: {name}",
599
+ "ag.copied": "Copied",
600
+ "ag.tabs": "Open sessions",
601
+ "ag.hint.focus":
602
+ "Click shows one session. Shift- or ⌘-click adds it alongside.",
603
+ "ag.hint.tabs":
604
+ "Click opens a session as a tab. Double-click a tab to rename it.",
605
+ "ag.hint.tile": "Click adds a session as a tile, up to 4.",
606
+ "ag.renameTip": "Double-click to rename",
607
+ "ag.renameName": "Rename {id}",
588
608
  "ag.rt.ready": "Starts {name} {version} from",
589
609
  "ag.rt.readyNoVer": "Starts {name} from",
590
610
  "ag.rt.notFound": "{name} not found.",
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The Agents tab's terminal input and links (AG-4b), pure helpers for
3
+ * agents-page.js (and its tests):
4
+ *
5
+ * isWebUrl(uri) only http: and https: open (an OSC 8 link's
6
+ * javascript:, data:, file: and the rest never)
7
+ * linkModifier(e, mac) the click that opens a link: ⌘ on a Mac, Ctrl
8
+ * elsewhere; a plain click still selects
9
+ * findPaths(line) file paths in a line of output: `src/foo.ts:42`,
10
+ * `./a/b.js:3:7`, `/abs/path`; never a URL
11
+ * editorUrl(ed, abs, l) an editor's URL for a path and line (vscode,
12
+ * cursor, zed); none: null (the path is copied)
13
+ * softNewlineKey(e) Shift+Enter, and no other modifier
14
+ * layoutKeyIndex(e) ⌃⇧1 / ⌃⇧2 / ⌃⇧3 (by the key's code: Shift+1 is
15
+ * "!" on a US keyboard): 0-2, else -1. ⌘1-9 and
16
+ * Ctrl+1-9 are the browser's (its tabs), never ours
17
+ */
18
+
19
+ /** The editors a path link opens in, by URL scheme (Settings › editor.open). */
20
+ export const EDITORS = ["vscode", "cursor", "zed", "none"];
21
+
22
+ /** Whether `uri` is an http or https URL: the only links that open. */
23
+ export function isWebUrl(uri) {
24
+ if (typeof uri !== "string" || !uri) return false;
25
+ let u;
26
+ try {
27
+ u = new URL(uri);
28
+ } catch {
29
+ return false;
30
+ }
31
+ return (u.protocol === "http:" || u.protocol === "https:") && !!u.host;
32
+ }
33
+
34
+ /** This browser is on a Mac (⌘ opens links there, Ctrl elsewhere). */
35
+ export function isMac(nav = globalThis.navigator) {
36
+ const p = nav?.userAgentData?.platform || nav?.platform || "";
37
+ return /mac|iphone|ipad/i.test(p);
38
+ }
39
+
40
+ /** The click opens a link: ⌘-click on a Mac, Ctrl-click elsewhere. */
41
+ export function linkModifier(e, mac = isMac()) {
42
+ if (!e) return false;
43
+ return mac ? !!e.metaKey : !!e.ctrlKey;
44
+ }
45
+
46
+ /**
47
+ * A path, then an optional :line and :column. It never starts right after
48
+ * a word character, a dot, a slash or a colon, so no part of a URL
49
+ * (https://host/a/b) and no part of a longer word is one.
50
+ */
51
+ const PATH_RE =
52
+ /(?<![\w./~:@%+-])((?:\.{1,2}\/|\/)?[\w@+-][\w.@+-]*(?:\/[\w.@+-]+)*)(?::(\d+)(?::(\d+))?)?/g;
53
+ const EXT_RE = /[^/.]\.[A-Za-z0-9]{1,12}$/;
54
+
55
+ /**
56
+ * The file paths in `line`: { text, start, end, path, line?, col? }, start
57
+ * and end its columns in the line (end exclusive). A path has a "/" and
58
+ * either starts with /, ./ or ../, has an extension or a line; or it is a
59
+ * file name with an extension and a line (`foo.ts:12`). The server says
60
+ * which of them exist in the agent's workspace (only those are links).
61
+ */
62
+ export function findPaths(line) {
63
+ const out = [];
64
+ if (typeof line !== "string" || !line) return out;
65
+ PATH_RE.lastIndex = 0;
66
+ for (let m; (m = PATH_RE.exec(line)); ) {
67
+ let p = m[1];
68
+ const ln = m[2] === undefined ? undefined : Number(m[2]);
69
+ const col = m[3] === undefined ? undefined : Number(m[3]);
70
+ let text = m[0];
71
+ if (ln === undefined) {
72
+ // A sentence's full stop is not the path's.
73
+ const trimmed = p.replace(/\.+$/, "");
74
+ text = trimmed;
75
+ p = trimmed;
76
+ }
77
+ if (!p) continue;
78
+ const slash = p.includes("/");
79
+ const rooted = /^(?:\/|\.{1,2}\/)/.test(p);
80
+ const ext = EXT_RE.test(p);
81
+ const ok = slash
82
+ ? rooted || ext || ln !== undefined
83
+ ? !/^\d+\/\d+$/.test(p)
84
+ : false
85
+ : ext && ln !== undefined;
86
+ if (!ok) continue;
87
+ out.push({
88
+ text,
89
+ start: m.index,
90
+ end: m.index + text.length,
91
+ path: p,
92
+ ...(ln !== undefined ? { line: ln } : {}),
93
+ ...(col !== undefined ? { col } : {}),
94
+ });
95
+ }
96
+ return out;
97
+ }
98
+
99
+ /**
100
+ * The URL that opens `abs` at `line` (and `col`) in `editor`, or null:
101
+ * none, an editor Den doesn't know, or a path that isn't absolute.
102
+ */
103
+ export function editorUrl(editor, abs, line, col) {
104
+ if (!["vscode", "cursor", "zed"].includes(editor)) return null;
105
+ if (typeof abs !== "string" || !abs.startsWith("/")) return null;
106
+ const p = abs.split("/").map(encodeURIComponent).join("/");
107
+ const at = line ? `:${line}${col ? `:${col}` : ""}` : "";
108
+ return `${editor}://file${p}${at}`;
109
+ }
110
+
111
+ /** Shift+Enter, no other modifier: the soft newline. */
112
+ export function softNewlineKey(e) {
113
+ return (
114
+ e?.type === "keydown" &&
115
+ e.key === "Enter" &&
116
+ e.shiftKey &&
117
+ !e.ctrlKey &&
118
+ !e.altKey &&
119
+ !e.metaKey &&
120
+ !e.isComposing
121
+ );
122
+ }
123
+
124
+ /** ⌃⇧1, ⌃⇧2, ⌃⇧3: the layouts' index; else -1. */
125
+ export function layoutKeyIndex(e) {
126
+ if (e?.type !== "keydown") return -1;
127
+ if (!e.ctrlKey || !e.shiftKey || e.metaKey || e.altKey) return -1;
128
+ return ["Digit1", "Digit2", "Digit3"].indexOf(e.code);
129
+ }
@@ -877,14 +877,32 @@ function paletteItems() {
877
877
  });
878
878
  }
879
879
  for (const x of palSessions) {
880
- const name = x.kind === "agent" ? (x.persona ?? x.label) : x.label;
880
+ // AG-4b: the name a person gave the agent, else its persona.
881
+ const base =
882
+ x.kind === "agent" ? x.displayName || (x.persona ?? x.label) : x.label;
883
+ // AG-4b: the session's name (a /rename, or Den's), when it says more.
884
+ const sn = x.kind === "agent" ? (x.sessionName ?? "").trim() : "";
885
+ const extra =
886
+ sn && sn !== base && sn !== x.persona && sn !== x.label ? sn : "";
887
+ const name = extra ? `${base} · ${extra}` : base;
881
888
  items.push({
882
889
  key: `agent:${x.id}`,
883
890
  kind: "agent",
884
891
  label: t("ag.pal", { ws: x.workspace, name }),
885
- sub: x.kind === "agent" && x.persona !== x.label ? x.label : undefined,
886
- dot: x.state === "running" ? "" : "off",
887
- keywords: [x.workspace, x.label, x.persona ?? "", "agents", "terminal"],
892
+ sub:
893
+ x.kind === "agent" && (x.displayName || x.persona !== x.label)
894
+ ? x.label
895
+ : undefined,
896
+ dot: x.waiting ? "warn" : x.state === "running" ? "" : "off",
897
+ keywords: [
898
+ x.workspace,
899
+ x.label,
900
+ x.persona ?? "",
901
+ x.displayName ?? "",
902
+ x.sessionName ?? "",
903
+ "agents",
904
+ "terminal",
905
+ ],
888
906
  run: () => go(`/agents?open=${encodeURIComponent(x.id)}`),
889
907
  });
890
908
  }
@@ -148,6 +148,10 @@ export const ROUTES: readonly RouteEntry[] = [
148
148
  "/api/agents/runtimes",
149
149
  "/api/workspaces/:ws/agents",
150
150
  ].map((p) => R(p)),
151
+ R(
152
+ "/api/workspaces/:ws/agents/:id/paths",
153
+ "which paths an agent printed exist in its workspace (stat only, AG-4b)",
154
+ ),
151
155
  R(
152
156
  "/api/sessions",
153
157
  "lists the sessions and agents (names and states, never their output)",
@@ -302,6 +306,11 @@ export const ROUTES: readonly RouteEntry[] = [
302
306
  M(["POST", "DELETE"], "/api/workspaces/services"),
303
307
  M(["POST"], "/api/workspaces/settings"),
304
308
  M(["POST"], "/api/workspaces/:ws/agents/:id/stop", "TERM to its session"),
309
+ M(
310
+ ["POST"],
311
+ "/api/workspaces/:ws/agents/:id/name",
312
+ "the name a person gave an agent (AG-4b); its id never changes",
313
+ ),
305
314
  M(
306
315
  ["POST"],
307
316
  "/api/workspaces/:ws/agents/:id/remove",
@@ -407,6 +416,8 @@ export const REGEX_ROUTES: Readonly<Record<string, readonly string[]>> = {
407
416
  "/api/workspaces/:ws/agents/:id/fresh",
408
417
  "/api/workspaces/:ws/agents/:id/relaunch",
409
418
  "/api/workspaces/:ws/agents/:id/remove",
419
+ "/api/workspaces/:ws/agents/:id/name",
420
+ "/api/workspaces/:ws/agents/:id/paths",
410
421
  ],
411
422
  "agent-routes.ts:RUNTIME_PATH": ["/api/agents/runtimes/:id/path"],
412
423
  "session-routes.ts:IO_PATH": ["/api/sessions/:id/io"],
@@ -43,6 +43,8 @@ import { getStateDir } from "../../shared/paths.js";
43
43
  import { product } from "../../shared/product.js";
44
44
  import { revokedAgentIds } from "../../shared/actor-token.js";
45
45
  import { listRecords } from "../../shared/agents/records.js";
46
+ import { softNewlineSeq } from "../../shared/agents/catalog.js";
47
+ import { getRuntime } from "../../shared/agents/runtimes.js";
46
48
  import type { RotationInfo } from "../../shared/agents/rotation.js";
47
49
  import {
48
50
  connectHost,
@@ -132,6 +134,18 @@ export interface ListedSession {
132
134
  /** The `<name>` part: an agent's id, a plain session's name. */
133
135
  label: string;
134
136
  agent?: string;
137
+ /** The name a person gave the agent (AG-4b), when one did. */
138
+ displayName?: string;
139
+ /**
140
+ * Its session's name (AG-4b): the latest of Den's rename and the
141
+ * runtime's (Claude Code's /rename), when there is one.
142
+ */
143
+ sessionName?: string;
144
+ /**
145
+ * What Shift+Enter sends to it (AG-4b, its catalog entry's softNewline):
146
+ * ESC CR unless the runtime says otherwise. A plain session: none (Enter).
147
+ */
148
+ softNewline?: string;
135
149
  persona?: string;
136
150
  runtime?: string;
137
151
  model?: string;
@@ -193,6 +207,9 @@ export function listedSessions(
193
207
  kind: "agent",
194
208
  label: rec.id,
195
209
  agent: rec.id,
210
+ ...(rec.displayName ? { displayName: rec.displayName } : {}),
211
+ ...(r.sessionName ? { sessionName: r.sessionName } : {}),
212
+ softNewline: softNewlineSeq(getRuntime(rec.runtime)?.catalog),
196
213
  persona: rec.persona,
197
214
  runtime: rec.runtime,
198
215
  ...(rec.model ? { model: rec.model } : {}),