@monster-orange/den 0.4.10 → 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.
- package/CHANGELOG.md +92 -0
- package/README.md +7 -7
- package/docs/distributions.md +67 -3
- package/package.json +2 -1
- package/src/shared/agents/catalog.ts +77 -0
- package/src/shared/agents/den-claude.ts +36 -8
- package/src/shared/agents/records.ts +29 -0
- package/src/shared/agents/runtime.ts +8 -0
- package/src/shared/agents/session-name.ts +110 -0
- package/src/shared/agents/transcript-follow.ts +180 -0
- package/src/shared/den-config.ts +7 -0
- package/src/ui/cli/agent-ops.ts +49 -0
- package/src/ui/server/agent-paths.ts +52 -0
- package/src/ui/server/agent-routes.ts +76 -5
- package/src/ui/server/agents-layout-pref.ts +43 -0
- package/src/ui/server/editor-open-pref.ts +41 -0
- package/src/ui/server/index.ts +22 -0
- package/src/ui/server/public/agents-page.js +539 -20
- package/src/ui/server/public/agents-section.js +1 -1
- package/src/ui/server/public/agents.html +178 -5
- package/src/ui/server/public/header.js +30 -19
- package/src/ui/server/public/i18n/es.js +23 -5
- package/src/ui/server/public/i18n/ja.js +23 -5
- package/src/ui/server/public/local-dev.html +1 -1
- package/src/ui/server/public/settings-registry.js +44 -0
- package/src/ui/server/public/shell.js +34 -20
- package/src/ui/server/public/term-input.js +129 -0
- package/src/ui/server/public/topbar.js +22 -4
- package/src/ui/server/public/workspace-picker.js +9 -6
- package/src/ui/server/route-classes.ts +11 -0
- package/src/ui/server/session-routes.ts +17 -0
- package/src/ui/server/ui-assets.ts +5 -0
|
@@ -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
|
-
|
|
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:
|
|
886
|
-
|
|
887
|
-
|
|
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
|
}
|
|
@@ -25,7 +25,9 @@ export const EN_FALLBACK = {
|
|
|
25
25
|
"ws.viewTitle":
|
|
26
26
|
"View: what every page shows ({ws}). A view filter only: it starts, stops and routes nothing.",
|
|
27
27
|
"ws.viewTitleFocus":
|
|
28
|
-
"View: what every page shows ({ws}).
|
|
28
|
+
"View: what every page shows ({ws}). It starts, stops and routes nothing. CLI default: {focus}.",
|
|
29
|
+
"ws.pickTip":
|
|
30
|
+
"Picking a workspace here also makes it the CLI default, so __DEN_CLI__ commands run there.",
|
|
29
31
|
};
|
|
30
32
|
function enT(key, vars) {
|
|
31
33
|
return String(EN_FALLBACK[key] ?? key).replace(/\{(\w+)\}/g, (m, k) =>
|
|
@@ -50,10 +52,10 @@ function focusName(data) {
|
|
|
50
52
|
/**
|
|
51
53
|
* The top bar's Workspace ▾ menu (R4, 0.3.0-dev.18), from the `/api/scope`
|
|
52
54
|
* and `/api/workspaces` payloads. It is the view scope: what every page,
|
|
53
|
-
* the health row and `status` show. Choosing
|
|
54
|
-
* focus (the default
|
|
55
|
-
*
|
|
56
|
-
*
|
|
55
|
+
* the health row and `status` show. Choosing a workspace also makes it the
|
|
56
|
+
* focus (the CLI default, Ward's decision in 0.4.11); "All active" leaves
|
|
57
|
+
* the focus where it is. The focused workspace is marked, and `focus`
|
|
58
|
+
* names it, so the caller can skip a choice that is already the focus.
|
|
57
59
|
*
|
|
58
60
|
* "All active" is offered with two or more active workspaces; with one,
|
|
59
61
|
* it and that workspace are the same view.
|
|
@@ -85,12 +87,13 @@ export function buildScopeMenuModel(scope, data, t = enT) {
|
|
|
85
87
|
label: n,
|
|
86
88
|
checked: n === one,
|
|
87
89
|
focused: n === focus,
|
|
90
|
+
title: t("ws.pickTip"),
|
|
88
91
|
})),
|
|
89
92
|
];
|
|
90
93
|
return {
|
|
91
94
|
label: active.length === 0 ? t("ws.noneLabel") : (one ?? t("ws.all")),
|
|
92
95
|
items,
|
|
93
|
-
|
|
96
|
+
focus: focus ?? null,
|
|
94
97
|
title: t(focus ? "ws.viewTitleFocus" : "ws.viewTitle", {
|
|
95
98
|
ws: inView.join(", ") || t("ws.noneLabel"),
|
|
96
99
|
focus,
|
|
@@ -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 } : {}),
|
|
@@ -36,6 +36,8 @@ export const UI_MODULES: readonly string[] = [
|
|
|
36
36
|
"/agents-page.js",
|
|
37
37
|
// The Start agent form's choices, shared by both (AG-10.0).
|
|
38
38
|
"/agent-start.js",
|
|
39
|
+
// AG-4b: the panes' keys and links (Shift+Enter, ⌃⇧1-3, paths).
|
|
40
|
+
"/term-input.js",
|
|
39
41
|
// SEC-2a: Pair a device's QR code (a tiny encoder of Den's own).
|
|
40
42
|
"/qr.js",
|
|
41
43
|
// The UI's catalogs other than English (R19; en is in shell.js).
|
|
@@ -51,6 +53,9 @@ export const VENDOR_ASSETS: Readonly<Record<string, string>> = {
|
|
|
51
53
|
"/vendor/xterm.mjs": "@xterm/xterm/lib/xterm.mjs",
|
|
52
54
|
"/vendor/xterm.css": "@xterm/xterm/css/xterm.css",
|
|
53
55
|
"/vendor/addon-fit.mjs": "@xterm/addon-fit/lib/addon-fit.mjs",
|
|
56
|
+
// AG-4b: plain URLs in a pane are links (⌘- or Ctrl-click opens them).
|
|
57
|
+
"/vendor/addon-web-links.mjs":
|
|
58
|
+
"@xterm/addon-web-links/lib/addon-web-links.mjs",
|
|
54
59
|
};
|
|
55
60
|
|
|
56
61
|
/** The installed file for a vendored URL path, or null. */
|