@nail00749/agent-gvozd 0.1.8 → 0.2.1

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/dist/tui.js ADDED
@@ -0,0 +1,1080 @@
1
+ // @bun
2
+ // src/tui.tsx
3
+ import { For as For2, Show as Show2, createSignal as createSignal2, onMount as onMount2 } from "solid-js";
4
+ import { Plugin, usePlugin as usePlugin2 } from "@opencode/plugin/tui";
5
+
6
+ // src/tui-insights.ts
7
+ import { createResource, createSignal, onCleanup, onMount } from "solid-js";
8
+ import { usePlugin } from "@opencode/plugin/tui";
9
+
10
+ // src/permissions-rpc.ts
11
+ import { Rpc } from "@opencode/plugin/rpc";
12
+
13
+ // src/tool-permissions.ts
14
+ function family(command, ...variants) {
15
+ return [command, ...variants].map((entry) => ({
16
+ exact: entry,
17
+ wildcard: `${entry} *`
18
+ }));
19
+ }
20
+ function exactOnly(command, ...variants) {
21
+ return [command, ...variants].map((entry) => ({ exact: entry, wildcard: entry }));
22
+ }
23
+ var INSPECTION_COMMANDS = [
24
+ ...family("pwd", "true", "test"),
25
+ ...family("cat", "head", "tail", "wc", "sort", "uniq"),
26
+ ...family("grep", "rg", "find", "diff", "cmp"),
27
+ ...family("ls", "du", "df", "stat", "file", "realpath", "basename", "dirname"),
28
+ ...family("shasum", "sha256sum", "md5sum"),
29
+ ...family("uname", "whoami", "hostname", "date", "printenv"),
30
+ ...family("which", "command -v"),
31
+ ...family("mktemp"),
32
+ ...family("tr", "cut", "paste", "column"),
33
+ ...family("node --version", "python3 --version", "python --version", "deno --version")
34
+ ];
35
+ var TOOLCHAIN_COMMANDS = [
36
+ ...family("bun test", "bun run test", "bun --version"),
37
+ ...family("bun run typecheck", "bun run lint", "bun run build", "bun run check"),
38
+ ...family("tsc --noEmit", "npx tsc --noEmit"),
39
+ ...family("eslint", "biome check", "prettier --check"),
40
+ ...family("npm test", "npm run test", "npm run typecheck", "npm run lint", "npm run build"),
41
+ ...family("pnpm test", "pnpm run test", "pnpm run build"),
42
+ ...family("yarn test", "yarn build"),
43
+ ...family("vitest run", "jest", "playwright test"),
44
+ ...family("cargo check", "cargo test", "cargo build", "cargo clippy", "cargo fmt --check", "cargo --version"),
45
+ ...family("go build ./...", "go test ./...", "go vet ./...", "go version"),
46
+ ...family("pytest", "python3 -m pytest", "python -m pytest"),
47
+ ...family("ruff check", "mypy", "pyright"),
48
+ ...family("mvn test", "mvn verify", "gradle test", "gradle check", "./gradlew test", "./gradlew check"),
49
+ ...family("make test", "make check", "make build", "make --version"),
50
+ ...family("just --list")
51
+ ];
52
+ var GIT_READONLY_COMMANDS = [
53
+ ...family("git status", "git status --short", "git status --short --branch", "git status --porcelain", "git status --porcelain=v1 --branch"),
54
+ ...family("git diff", "git diff --stat", "git diff --cached", "git diff --check"),
55
+ ...family("git log", "git show"),
56
+ ...family("git rev-parse", "git rev-list", "git show-ref", "git cat-file"),
57
+ ...exactOnly("git symbolic-ref HEAD", "git symbolic-ref --short HEAD"),
58
+ ...family("git ls-files", "git ls-remote", "git grep"),
59
+ ...exactOnly("git branch", "git tag", "git remote", "git reflog"),
60
+ ...family("git branch --list", "git branch -l", "git branch -a", "git branch -r", "git branch -v", "git branch -vv", "git branch --all", "git branch --remotes", "git branch --show-current", "git branch --contains"),
61
+ ...family("git tag --list", "git tag -l", "git tag -n"),
62
+ ...family("git remote -v", "git remote --verbose", "git remote show", "git remote get-url"),
63
+ ...family("git reflog show"),
64
+ ...family("git stash list", "git describe", "git worktree list"),
65
+ ...family("git config --get", "git config --get-regexp")
66
+ ];
67
+ var GIT_MUTATING_COMMANDS = [
68
+ ...family("git add", "git rm --cached"),
69
+ ...family("git commit", "git merge --ff-only", "git merge --no-ff"),
70
+ ...family("git push", "git fetch", "git pull --ff-only"),
71
+ ...family("git stash", "git cherry-pick", "git revert"),
72
+ ...family("git switch", "git checkout -b", "git worktree add"),
73
+ ...exactOnly("git branch *", "git tag *", "git remote *", "git symbolic-ref *", "git reflog *")
74
+ ];
75
+
76
+ // src/permissions-rpc.ts
77
+ var GvozdPermissions = Rpc.define({
78
+ id: "gvozd-permissions",
79
+ events: {},
80
+ methods: {
81
+ evaluate: {
82
+ input: {
83
+ type: "object",
84
+ properties: {
85
+ agent: { type: "string" },
86
+ checks: {
87
+ type: "array",
88
+ items: {
89
+ type: "object",
90
+ properties: {
91
+ action: { type: "string" },
92
+ resources: { type: "array", items: { type: "string" } }
93
+ },
94
+ required: ["action", "resources"],
95
+ additionalProperties: false
96
+ }
97
+ }
98
+ },
99
+ required: ["agent", "checks"],
100
+ additionalProperties: false
101
+ },
102
+ output: {
103
+ type: "object",
104
+ properties: {
105
+ results: {
106
+ type: "array",
107
+ items: {
108
+ type: "object",
109
+ properties: {
110
+ action: { type: "string" },
111
+ resource: { type: "string" },
112
+ effect: { type: "string", enum: ["allow", "ask", "deny", "unknown"] },
113
+ matchedRule: { type: ["string", "null"] }
114
+ },
115
+ required: ["action", "resource", "effect", "matchedRule"],
116
+ additionalProperties: false
117
+ }
118
+ }
119
+ },
120
+ required: ["results"],
121
+ additionalProperties: false
122
+ }
123
+ }
124
+ }
125
+ });
126
+ var GvozdLeases = Rpc.define({
127
+ id: "gvozd-leases",
128
+ events: {},
129
+ methods: {
130
+ list: {
131
+ input: {
132
+ type: "object",
133
+ properties: {},
134
+ additionalProperties: false
135
+ },
136
+ output: {
137
+ type: "object",
138
+ properties: {
139
+ leases: {
140
+ type: "array",
141
+ items: {
142
+ type: "object",
143
+ properties: {
144
+ leaseId: { type: "string" },
145
+ parentSessionID: { type: "string" },
146
+ sessionID: { type: "string" },
147
+ agent: { type: "string" },
148
+ label: { type: "string" },
149
+ state: { type: "string", enum: ["reserved", "active"] },
150
+ files: { type: "array", items: { type: "string" } },
151
+ expiresAt: { type: "number" },
152
+ lastActivityAt: { type: "number" }
153
+ },
154
+ required: ["leaseId", "parentSessionID", "agent", "label", "state", "files", "expiresAt", "lastActivityAt"],
155
+ additionalProperties: false
156
+ }
157
+ }
158
+ },
159
+ required: ["leases"],
160
+ additionalProperties: false
161
+ }
162
+ }
163
+ }
164
+ });
165
+
166
+ // src/trusted-mode.ts
167
+ import { Rpc as Rpc2 } from "@opencode/plugin/rpc";
168
+ var TRUST_MODES = ["balanced", "trusted", "strict"];
169
+ var GvozdMode = Rpc2.define({
170
+ id: "gvozd-mode",
171
+ events: {},
172
+ methods: {
173
+ set: {
174
+ input: {
175
+ type: "object",
176
+ properties: {
177
+ sessionID: { type: "string" },
178
+ mode: { type: "string", enum: [...TRUST_MODES] }
179
+ },
180
+ required: ["sessionID", "mode"],
181
+ additionalProperties: false
182
+ },
183
+ output: {
184
+ type: "object",
185
+ properties: {
186
+ mode: { type: "string", enum: [...TRUST_MODES] }
187
+ },
188
+ required: ["mode"],
189
+ additionalProperties: false
190
+ }
191
+ },
192
+ get: {
193
+ input: {
194
+ type: "object",
195
+ properties: { sessionID: { type: "string" } },
196
+ required: ["sessionID"],
197
+ additionalProperties: false
198
+ },
199
+ output: {
200
+ type: "object",
201
+ properties: {
202
+ mode: { type: "string", enum: [...TRUST_MODES] }
203
+ },
204
+ required: ["mode"],
205
+ additionalProperties: false
206
+ }
207
+ }
208
+ }
209
+ });
210
+
211
+ // src/session-insights.ts
212
+ function collectSkillUsages(messages) {
213
+ if (!messages || messages.length === 0)
214
+ return [];
215
+ const byName = new Map;
216
+ for (const message of messages) {
217
+ if (message.type !== "skill")
218
+ continue;
219
+ const name = message.name || message.skill;
220
+ if (!name)
221
+ continue;
222
+ const time = message.time?.created ?? 0;
223
+ const existing = byName.get(name);
224
+ if (existing) {
225
+ existing.activations += 1;
226
+ existing.lastUsedAt = Math.max(existing.lastUsedAt, time);
227
+ } else {
228
+ byName.set(name, { name, lastUsedAt: time, activations: 1 });
229
+ }
230
+ }
231
+ return [...byName.values()].sort((a, b) => b.lastUsedAt - a.lastUsedAt);
232
+ }
233
+ function collectPermissionUsages(pending, replies) {
234
+ const byID = new Map;
235
+ for (const request of pending ?? []) {
236
+ if (!request || typeof request.id !== "string" || request.id === "")
237
+ continue;
238
+ const resources = request.resources ?? [];
239
+ byID.set(request.id, {
240
+ id: request.id,
241
+ action: request.action ?? "",
242
+ resource: resources[0] ?? "(no resource)",
243
+ extraResources: Math.max(0, resources.length - 1),
244
+ pending: true
245
+ });
246
+ }
247
+ for (const reply of replies) {
248
+ const answered = {
249
+ id: reply.id,
250
+ action: reply.action ?? "",
251
+ resource: reply.resources?.[0] ?? "(no resource)",
252
+ extraResources: Math.max(0, (reply.resources?.length ?? 1) - 1),
253
+ pending: false,
254
+ reply: reply.reply,
255
+ repliedAt: reply.time
256
+ };
257
+ byID.set(reply.id, answered);
258
+ }
259
+ const entries = [...byID.values()];
260
+ return entries.sort((a, b) => {
261
+ if (a.pending !== b.pending)
262
+ return a.pending ? -1 : 1;
263
+ return (b.repliedAt ?? 0) - (a.repliedAt ?? 0);
264
+ });
265
+ }
266
+
267
+ // src/session-tools.ts
268
+ function collectSessionTree(rootSessionID, family, status) {
269
+ if (!family || family.length === 0)
270
+ return [];
271
+ const nodes = [];
272
+ for (const session of family) {
273
+ if (!session || typeof session.id !== "string")
274
+ continue;
275
+ nodes.push({
276
+ sessionID: session.id,
277
+ agent: session.agent,
278
+ model: session.model ? `${session.model.providerID}/${session.model.id}` : undefined,
279
+ title: session.title,
280
+ status: status(session.id) ?? "idle",
281
+ cost: session.cost ?? 0,
282
+ tokens: (session.tokens?.input ?? 0) + (session.tokens?.output ?? 0) + (session.tokens?.reasoning ?? 0),
283
+ outcome: session.outcome,
284
+ isRoot: session.id === rootSessionID
285
+ });
286
+ }
287
+ nodes.sort((a, b) => a.isRoot === b.isRoot ? 0 : a.isRoot ? -1 : 1);
288
+ return nodes;
289
+ }
290
+ var RECENT_ERROR_LIMIT = 5;
291
+ function collectToolStats(messages) {
292
+ const counts = new Map;
293
+ let errors = 0;
294
+ const recentErrors = [];
295
+ if (!messages) {
296
+ return { counts, totalCalls: 0, errors: 0, recentErrors };
297
+ }
298
+ for (const message of messages) {
299
+ if (message.type !== "assistant")
300
+ continue;
301
+ for (const part of message.content ?? []) {
302
+ if (part.type !== "tool")
303
+ continue;
304
+ counts.set(part.name, (counts.get(part.name) ?? 0) + 1);
305
+ if (part.state?.status !== "error")
306
+ continue;
307
+ errors += 1;
308
+ const raw = part.state.error;
309
+ const message_ = typeof raw === "string" ? raw : raw?.message ?? "";
310
+ const permission = /permission|denied|rejected/i.test(`${raw?.type ?? ""} ${message_}`);
311
+ recentErrors.push({
312
+ tool: part.name,
313
+ message: message_.replace(/\s+/g, " ").slice(0, 160),
314
+ permission,
315
+ time: message.time?.created ?? 0
316
+ });
317
+ }
318
+ }
319
+ recentErrors.sort((a, b) => b.time - a.time);
320
+ return {
321
+ counts,
322
+ totalCalls: [...counts.values()].reduce((sum, count) => sum + count, 0),
323
+ errors,
324
+ recentErrors: recentErrors.slice(0, RECENT_ERROR_LIMIT)
325
+ };
326
+ }
327
+ function formatFooterStatus(permissions, tree) {
328
+ const pending = permissions.filter((entry) => entry.pending).length;
329
+ const running = tree.filter((node) => !node.isRoot && node.status === "running").length;
330
+ const cost = tree.reduce((sum, node) => sum + node.cost, 0);
331
+ return [
332
+ pending > 0 ? `\u23F3${pending} perm` : "",
333
+ running > 0 ? `\u25CF${running} agents` : "",
334
+ cost > 0 ? `$${cost.toFixed(2)}` : ""
335
+ ].filter(Boolean).join(" \xB7 ");
336
+ }
337
+ function topTools(stats, limit = 5) {
338
+ return [...stats.counts.entries()].map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)).slice(0, limit);
339
+ }
340
+
341
+ // src/tui-insights.ts
342
+ var EMPTY_INSIGHTS = {
343
+ tree: [],
344
+ skills: [],
345
+ permissions: [],
346
+ tools: { counts: new Map, totalCalls: 0, errors: 0, recentErrors: [] }
347
+ };
348
+ function themeColor(theme, path, fallback = "#808080") {
349
+ let current = theme;
350
+ for (const key of path) {
351
+ if (typeof current !== "object" || current === null)
352
+ return fallback;
353
+ current = current[key];
354
+ }
355
+ return typeof current === "string" ? current : fallback;
356
+ }
357
+ function relativeTime(timestamp, now) {
358
+ if (!timestamp)
359
+ return "";
360
+ const seconds = Math.max(0, Math.round((now - timestamp) / 1000));
361
+ if (seconds < 60)
362
+ return `${seconds}s`;
363
+ const minutes = Math.round(seconds / 60);
364
+ if (minutes < 60)
365
+ return `${minutes}m`;
366
+ const hours = Math.round(minutes / 60);
367
+ if (hours < 24)
368
+ return `${hours}h`;
369
+ return `${Math.round(hours / 24)}d`;
370
+ }
371
+ function useSessionInsights(sessionID) {
372
+ const context = usePlugin();
373
+ const [log, updateLog] = context.storage.store("gvozd.tui.permission-replies", {
374
+ initial: { replies: {} }
375
+ });
376
+ const replies = () => Object.values(log.replies);
377
+ const recordReply = (record) => {
378
+ updateReply(record);
379
+ };
380
+ const updateReply = async (record) => {
381
+ await updateLog((draft) => {
382
+ draft.replies[record.id] = record;
383
+ });
384
+ };
385
+ const [version, setVersion] = createSignal(0);
386
+ onMount(() => {
387
+ const stops = [
388
+ context.data.on("session.skill.activated", (event) => {
389
+ if (event.data?.sessionID !== sessionID())
390
+ return;
391
+ context.data.session.message.invalidate(event.data.sessionID);
392
+ setVersion((value) => value + 1);
393
+ }),
394
+ context.data.on("permission.asked", (event) => {
395
+ if (event.data?.sessionID !== sessionID())
396
+ return;
397
+ context.data.session.permission.sync(event.data.sessionID);
398
+ setVersion((value) => value + 1);
399
+ }),
400
+ context.data.on("permission.replied", (event) => {
401
+ if (event.data?.sessionID !== sessionID())
402
+ return;
403
+ const data = event.data;
404
+ recordReply({
405
+ id: data.requestID,
406
+ reply: data.reply,
407
+ time: event.created ?? Date.now()
408
+ });
409
+ context.data.session.permission.invalidate(data.sessionID);
410
+ setVersion((value) => value + 1);
411
+ }),
412
+ context.data.on("session.execution.succeeded", (event) => {
413
+ if (event.data?.sessionID !== sessionID())
414
+ return;
415
+ context.data.session.message.invalidate(event.data.sessionID);
416
+ setVersion((value) => value + 1);
417
+ })
418
+ ];
419
+ onCleanup(() => {
420
+ for (const stop of stops)
421
+ stop();
422
+ });
423
+ });
424
+ const [resource] = createResource(() => {
425
+ const id = sessionID();
426
+ return { id, revision: version() };
427
+ }, async (key) => {
428
+ const id = key.id;
429
+ if (!id)
430
+ return EMPTY_INSIGHTS;
431
+ const [messages, pending] = await Promise.allSettled([
432
+ context.data.session.message.sync(id),
433
+ context.data.session.permission.sync(id)
434
+ ]);
435
+ if (messages.status === "rejected") {
436
+ console.error("gvozd tui: message sync failed", messages.reason);
437
+ }
438
+ if (pending.status === "rejected") {
439
+ console.error("gvozd tui: permission sync failed", pending.reason);
440
+ }
441
+ const sessionList = context.data.session.message.list(id);
442
+ const family = (sessionID) => {
443
+ const members = [sessionID, ...context.data.session.family(sessionID).filter((member) => member !== sessionID)];
444
+ const resolved = [];
445
+ for (const member of members) {
446
+ const info = context.data.session.get(member);
447
+ if (info)
448
+ resolved.push(info);
449
+ }
450
+ return resolved;
451
+ };
452
+ return {
453
+ tree: collectSessionTree(id, family(id), (memberID) => context.data.session.status(memberID) ?? "idle"),
454
+ skills: collectSkillUsages(sessionList),
455
+ permissions: collectPermissionUsages(context.data.session.permission.list(id), replies()),
456
+ tools: collectToolStats(sessionList)
457
+ };
458
+ }, { initialValue: EMPTY_INSIGHTS });
459
+ return resource;
460
+ }
461
+ async function setTrustMode(sessionID, mode) {
462
+ const context = usePlugin();
463
+ try {
464
+ const rpc = context.client.rpc(GvozdMode);
465
+ return await rpc.set({ sessionID, mode });
466
+ } catch (error) {
467
+ console.error("gvozd tui: mode switch failed", error);
468
+ return;
469
+ }
470
+ }
471
+ async function listLeases() {
472
+ const context = usePlugin();
473
+ try {
474
+ const rpc = context.client.rpc(GvozdLeases);
475
+ return await rpc.list();
476
+ } catch (error) {
477
+ console.error("gvozd tui: lease list failed", error);
478
+ return;
479
+ }
480
+ }
481
+ async function evaluatePermissions(agent, checks) {
482
+ const context = usePlugin();
483
+ try {
484
+ const rpc = context.client.rpc(GvozdPermissions);
485
+ const input = { agent, checks };
486
+ return await rpc.evaluate(input);
487
+ } catch (error) {
488
+ console.error("gvozd tui: permission dry-run failed", error);
489
+ return;
490
+ }
491
+ }
492
+
493
+ // src/command-pipeline.ts
494
+ function splitCommandPipeline(input) {
495
+ const segments = [];
496
+ let current = "";
497
+ let escaped = false;
498
+ const contexts = [{ parenDepth: 0 }];
499
+ let heredoc;
500
+ const flush = () => {
501
+ const trimmed = current.trim();
502
+ if (trimmed)
503
+ segments.push(trimmed);
504
+ current = "";
505
+ };
506
+ const at = (index) => input[index] ?? "";
507
+ for (let index = 0;index < input.length; index++) {
508
+ const character = input[index];
509
+ if (heredoc) {
510
+ if (character === `
511
+ `) {
512
+ const line = current.slice(current.lastIndexOf(`
513
+ `) + 1).replace(/\r$/, "");
514
+ const candidate = heredoc.stripTabs ? line.replace(/^\t+/, "") : line;
515
+ if (candidate === heredoc.marker) {
516
+ heredoc = undefined;
517
+ current += character;
518
+ flush();
519
+ continue;
520
+ }
521
+ }
522
+ current += character;
523
+ continue;
524
+ }
525
+ const context = contexts[contexts.length - 1];
526
+ if (escaped) {
527
+ current += character;
528
+ escaped = false;
529
+ continue;
530
+ }
531
+ if (character === "\\" && context.quote !== "'") {
532
+ current += character;
533
+ escaped = true;
534
+ continue;
535
+ }
536
+ if (character === '"' || character === "'") {
537
+ if (context.quote === character)
538
+ context.quote = undefined;
539
+ else if (!context.quote)
540
+ context.quote = character;
541
+ current += character;
542
+ continue;
543
+ }
544
+ if (context.quote === "'") {
545
+ current += character;
546
+ continue;
547
+ }
548
+ if (character === "$" && at(index + 1) === "(") {
549
+ contexts.push({ closer: ")", parenDepth: 0 });
550
+ current += "$(";
551
+ index++;
552
+ continue;
553
+ }
554
+ if (character === "`") {
555
+ if (context.closer === "`")
556
+ contexts.pop();
557
+ else
558
+ contexts.push({ closer: "`", parenDepth: 0 });
559
+ current += character;
560
+ continue;
561
+ }
562
+ if (context.closer === ")" && character === "(") {
563
+ context.parenDepth++;
564
+ current += character;
565
+ continue;
566
+ }
567
+ if (context.closer === ")" && character === ")") {
568
+ if (context.parenDepth > 0)
569
+ context.parenDepth--;
570
+ else
571
+ contexts.pop();
572
+ current += character;
573
+ continue;
574
+ }
575
+ if (contexts.length > 1 || context.quote) {
576
+ current += character;
577
+ continue;
578
+ }
579
+ if (character === "<" && at(index - 1) !== "<" && at(index + 1) === "<" && at(index + 2) !== "<") {
580
+ let cursor = index + 2;
581
+ const stripTabs = input[cursor] === "-";
582
+ if (stripTabs)
583
+ cursor++;
584
+ let marker = "";
585
+ while (cursor < input.length && /[ \t]/.test(input[cursor]))
586
+ cursor++;
587
+ if (input[cursor] === '"' || input[cursor] === "'") {
588
+ const closeQuote = input[cursor];
589
+ cursor++;
590
+ while (cursor < input.length && input[cursor] !== closeQuote)
591
+ marker += input[cursor++];
592
+ if (input[cursor] === closeQuote)
593
+ cursor++;
594
+ } else {
595
+ while (cursor < input.length && !/[\s;|&<>()]/.test(input[cursor]))
596
+ marker += input[cursor++];
597
+ }
598
+ if (marker) {
599
+ heredoc = { marker, stripTabs };
600
+ current += input.slice(index, cursor);
601
+ index = cursor - 1;
602
+ continue;
603
+ }
604
+ }
605
+ if (character === ";" || character === `
606
+ `) {
607
+ flush();
608
+ continue;
609
+ }
610
+ if (character === "&" || character === "|") {
611
+ flush();
612
+ if (input[index + 1] === character)
613
+ index++;
614
+ continue;
615
+ }
616
+ current += character;
617
+ }
618
+ flush();
619
+ return segments;
620
+ }
621
+
622
+ // src/tui.tsx
623
+ import { jsxDEV, Fragment } from "@opentui/solid/jsx-dev-runtime";
624
+ function SkillsSection(props) {
625
+ const context = usePlugin2();
626
+ const now = Date.now();
627
+ return /* @__PURE__ */ jsxDEV("box", {
628
+ flexDirection: "column",
629
+ children: [
630
+ /* @__PURE__ */ jsxDEV("text", {
631
+ fg: themeColor(context.theme, ["text", "muted"]),
632
+ children: "skills"
633
+ }, undefined, false, undefined, this),
634
+ /* @__PURE__ */ jsxDEV(For2, {
635
+ each: props.insights.skills.slice(0, 8),
636
+ children: (skill) => /* @__PURE__ */ jsxDEV("text", {
637
+ fg: themeColor(context.theme, ["text", "default"]),
638
+ children: `\u25B8 ${skill.name} ${relativeTime(skill.lastUsedAt, now)}`
639
+ }, undefined, false, undefined, this)
640
+ }, undefined, false, undefined, this)
641
+ ]
642
+ }, undefined, true, undefined, this);
643
+ }
644
+ function PermissionsSection(props) {
645
+ const context = usePlugin2();
646
+ return /* @__PURE__ */ jsxDEV("box", {
647
+ flexDirection: "column",
648
+ children: [
649
+ /* @__PURE__ */ jsxDEV("text", {
650
+ fg: themeColor(context.theme, ["text", "muted"]),
651
+ children: "permissions"
652
+ }, undefined, false, undefined, this),
653
+ /* @__PURE__ */ jsxDEV(For2, {
654
+ each: props.insights.permissions.slice(0, 8),
655
+ children: (entry) => {
656
+ const marker = entry.pending ? "\u23F3" : entry.reply === "reject" ? "\u2717" : "\u2713";
657
+ const fg = entry.pending ? themeColor(context.theme, ["text", "default"]) : entry.reply === "reject" ? themeColor(context.theme, ["status", "error"]) : entry.reply === "always" ? themeColor(context.theme, ["status", "success"]) : themeColor(context.theme, ["text", "muted"]);
658
+ const label = entry.pending ? "" : entry.reply === "always" ? " always" : entry.reply === "reject" ? " reject" : " once";
659
+ const extra = entry.extraResources > 0 ? ` +${entry.extraResources}` : "";
660
+ return /* @__PURE__ */ jsxDEV("text", {
661
+ fg,
662
+ children: `${marker} ${entry.action} | ${entry.resource}${extra}${label}`
663
+ }, undefined, false, undefined, this);
664
+ }
665
+ }, undefined, false, undefined, this)
666
+ ]
667
+ }, undefined, true, undefined, this);
668
+ }
669
+ function SubagentsSection(props) {
670
+ const context = usePlugin2();
671
+ const descendants = () => props.insights.tree.filter((node) => !node.isRoot);
672
+ return /* @__PURE__ */ jsxDEV(Show2, {
673
+ when: descendants().length > 0,
674
+ children: /* @__PURE__ */ jsxDEV("box", {
675
+ flexDirection: "column",
676
+ children: [
677
+ /* @__PURE__ */ jsxDEV("text", {
678
+ fg: themeColor(context.theme, ["text", "muted"]),
679
+ children: "subagents"
680
+ }, undefined, false, undefined, this),
681
+ /* @__PURE__ */ jsxDEV(For2, {
682
+ each: descendants().slice(0, 8),
683
+ children: (node) => {
684
+ const marker = node.status === "running" ? "\u25CF" : node.outcome === "failed" || node.outcome === "interrupted" ? "\u2717" : "\u25CB";
685
+ const fg = node.status === "running" ? themeColor(context.theme, ["status", "success"]) : node.outcome === "failed" || node.outcome === "interrupted" ? themeColor(context.theme, ["status", "error"]) : themeColor(context.theme, ["text", "muted"]);
686
+ const model = node.model ? ` ${node.model.split("/").pop()}` : "";
687
+ const cost = node.cost > 0 ? ` $${node.cost.toFixed(2)}` : "";
688
+ return /* @__PURE__ */ jsxDEV("text", {
689
+ fg,
690
+ children: `${marker} ${node.agent ?? "?"}${model}${cost}`
691
+ }, undefined, false, undefined, this);
692
+ }
693
+ }, undefined, false, undefined, this)
694
+ ]
695
+ }, undefined, true, undefined, this)
696
+ }, undefined, false, undefined, this);
697
+ }
698
+ function ToolsSection(props) {
699
+ const context = usePlugin2();
700
+ const ranked = () => topTools(props.insights.tools, 5);
701
+ return /* @__PURE__ */ jsxDEV(Show2, {
702
+ when: props.insights.tools.totalCalls > 0,
703
+ children: /* @__PURE__ */ jsxDEV("box", {
704
+ flexDirection: "column",
705
+ children: [
706
+ /* @__PURE__ */ jsxDEV("text", {
707
+ fg: themeColor(context.theme, ["text", "muted"]),
708
+ children: "tools"
709
+ }, undefined, false, undefined, this),
710
+ /* @__PURE__ */ jsxDEV(For2, {
711
+ each: ranked(),
712
+ children: (tool) => /* @__PURE__ */ jsxDEV("text", {
713
+ fg: themeColor(context.theme, ["text", "default"]),
714
+ children: `\u25B8 ${tool.name} \xD7${tool.count}`
715
+ }, undefined, false, undefined, this)
716
+ }, undefined, false, undefined, this),
717
+ /* @__PURE__ */ jsxDEV(Show2, {
718
+ when: props.insights.tools.errors > 0,
719
+ children: /* @__PURE__ */ jsxDEV("text", {
720
+ fg: themeColor(context.theme, ["status", "error"]),
721
+ children: `${props.insights.tools.errors} error(s), ${props.insights.tools.recentErrors.filter((error) => error.permission).length} permission`
722
+ }, undefined, false, undefined, this)
723
+ }, undefined, false, undefined, this)
724
+ ]
725
+ }, undefined, true, undefined, this)
726
+ }, undefined, false, undefined, this);
727
+ }
728
+ function SessionInsightsSlot(props) {
729
+ const insights = useSessionInsights(() => props.sessionID);
730
+ const current = () => insights.latest ?? EMPTY_INSIGHTS;
731
+ return /* @__PURE__ */ jsxDEV(Show2, {
732
+ when: current().skills.length > 0 || current().permissions.length > 0 || current().tree.some((node) => !node.isRoot) || current().tools.totalCalls > 0,
733
+ children: /* @__PURE__ */ jsxDEV("box", {
734
+ flexDirection: "column",
735
+ marginTop: 1,
736
+ children: [
737
+ /* @__PURE__ */ jsxDEV(Show2, {
738
+ when: current().tree.some((node) => !node.isRoot),
739
+ children: /* @__PURE__ */ jsxDEV(SubagentsSection, {
740
+ insights: current()
741
+ }, undefined, false, undefined, this)
742
+ }, undefined, false, undefined, this),
743
+ /* @__PURE__ */ jsxDEV(Show2, {
744
+ when: current().skills.length > 0,
745
+ children: /* @__PURE__ */ jsxDEV(SkillsSection, {
746
+ insights: current()
747
+ }, undefined, false, undefined, this)
748
+ }, undefined, false, undefined, this),
749
+ /* @__PURE__ */ jsxDEV(Show2, {
750
+ when: current().permissions.length > 0,
751
+ children: /* @__PURE__ */ jsxDEV(PermissionsSection, {
752
+ insights: current()
753
+ }, undefined, false, undefined, this)
754
+ }, undefined, false, undefined, this),
755
+ /* @__PURE__ */ jsxDEV(Show2, {
756
+ when: current().tools.totalCalls > 0,
757
+ children: /* @__PURE__ */ jsxDEV(ToolsSection, {
758
+ insights: current()
759
+ }, undefined, false, undefined, this)
760
+ }, undefined, false, undefined, this)
761
+ ]
762
+ }, undefined, true, undefined, this)
763
+ }, undefined, false, undefined, this);
764
+ }
765
+ function DryRunPanel() {
766
+ const context = usePlugin2();
767
+ const [input, setInput] = createSignal2("");
768
+ const [agent] = createSignal2("master");
769
+ const [rows, setRows] = createSignal2([]);
770
+ const [busy, setBusy] = createSignal2(false);
771
+ const run = async () => {
772
+ const command = input().trim();
773
+ if (!command || busy())
774
+ return;
775
+ setBusy(true);
776
+ try {
777
+ const segments = splitCommandPipeline(command);
778
+ const output = await evaluatePermissions(agent(), [{ action: "shell", resources: segments }]);
779
+ if (output) {
780
+ setRows((current) => [
781
+ ...output.results.map((result) => ({
782
+ command: result.resource,
783
+ effect: result.effect,
784
+ matchedRule: result.matchedRule
785
+ })),
786
+ ...current
787
+ ].slice(0, 20));
788
+ }
789
+ } finally {
790
+ setBusy(false);
791
+ }
792
+ };
793
+ return /* @__PURE__ */ jsxDEV("box", {
794
+ flexDirection: "column",
795
+ padding: 1,
796
+ children: [
797
+ /* @__PURE__ */ jsxDEV("text", {
798
+ fg: themeColor(context.theme, ["text", "default"]),
799
+ children: "gvozd permission dry-run"
800
+ }, undefined, false, undefined, this),
801
+ /* @__PURE__ */ jsxDEV("text", {
802
+ fg: themeColor(context.theme, ["text", "muted"]),
803
+ children: `agent: ${agent()} \u2014 type a shell command and press enter`
804
+ }, undefined, false, undefined, this),
805
+ /* @__PURE__ */ jsxDEV("input", {
806
+ placeholder: "git diff HEAD",
807
+ onInput: (value) => setInput(value),
808
+ onSubmit: () => void run()
809
+ }, undefined, false, undefined, this),
810
+ /* @__PURE__ */ jsxDEV(For2, {
811
+ each: rows(),
812
+ children: (row) => {
813
+ const fg = row.effect === "deny" ? themeColor(context.theme, ["status", "error"]) : row.effect === "allow" ? themeColor(context.theme, ["status", "success"]) : themeColor(context.theme, ["text", "default"]);
814
+ return /* @__PURE__ */ jsxDEV("text", {
815
+ fg,
816
+ children: `${row.effect.padEnd(7)} ${row.command}${row.matchedRule ? ` \u2190 ${row.matchedRule}` : ""}`
817
+ }, undefined, false, undefined, this);
818
+ }
819
+ }, undefined, false, undefined, this),
820
+ /* @__PURE__ */ jsxDEV(Show2, {
821
+ when: rows().length === 0 && !busy(),
822
+ children: /* @__PURE__ */ jsxDEV("text", {
823
+ fg: themeColor(context.theme, ["text", "muted"]),
824
+ children: "no evaluations yet"
825
+ }, undefined, false, undefined, this)
826
+ }, undefined, false, undefined, this)
827
+ ]
828
+ }, undefined, true, undefined, this);
829
+ }
830
+ function LeasePanel() {
831
+ const context = usePlugin2();
832
+ const [snapshot, setSnapshot] = createSignal2();
833
+ const [busy, setBusy] = createSignal2(false);
834
+ const refresh = async () => {
835
+ setBusy(true);
836
+ try {
837
+ setSnapshot(await listLeases());
838
+ } finally {
839
+ setBusy(false);
840
+ }
841
+ };
842
+ onMount2(() => void refresh());
843
+ return /* @__PURE__ */ jsxDEV("box", {
844
+ flexDirection: "column",
845
+ padding: 1,
846
+ children: [
847
+ /* @__PURE__ */ jsxDEV("text", {
848
+ fg: themeColor(context.theme, ["text", "default"]),
849
+ children: "gvozd file leases"
850
+ }, undefined, false, undefined, this),
851
+ /* @__PURE__ */ jsxDEV(Show2, {
852
+ when: !busy(),
853
+ fallback: /* @__PURE__ */ jsxDEV("text", {
854
+ fg: themeColor(context.theme, ["text", "muted"]),
855
+ children: "refreshing\u2026"
856
+ }, undefined, false, undefined, this),
857
+ children: /* @__PURE__ */ jsxDEV(Show2, {
858
+ when: (snapshot()?.leases.length ?? 0) > 0,
859
+ fallback: /* @__PURE__ */ jsxDEV("text", {
860
+ fg: themeColor(context.theme, ["text", "muted"]),
861
+ children: "no leases \u2014 writers run without reservations"
862
+ }, undefined, false, undefined, this),
863
+ children: /* @__PURE__ */ jsxDEV(For2, {
864
+ each: snapshot()?.leases ?? [],
865
+ children: (lease) => /* @__PURE__ */ jsxDEV("text", {
866
+ children: [
867
+ /* @__PURE__ */ jsxDEV("span", {
868
+ style: { fg: lease.state === "active" ? themeColor(context.theme, ["status", "success"]) : themeColor(context.theme, ["text", "muted"]) },
869
+ children: `${lease.state === "active" ? "\u25CF" : "\u25CB"} ${lease.agent} ${lease.label} ${lease.files.length}f `
870
+ }, undefined, false, undefined, this),
871
+ /* @__PURE__ */ jsxDEV("span", {
872
+ style: { fg: themeColor(context.theme, ["text", "muted"]) },
873
+ children: `ttl ${relativeTime(lease.expiresAt, Date.now())}`
874
+ }, undefined, false, undefined, this)
875
+ ]
876
+ }, undefined, true, undefined, this)
877
+ }, undefined, false, undefined, this)
878
+ }, undefined, false, undefined, this)
879
+ }, undefined, false, undefined, this)
880
+ ]
881
+ }, undefined, true, undefined, this);
882
+ }
883
+ function FullscreenPanel() {
884
+ const context = usePlugin2();
885
+ const [sessionID, setSessionID] = createSignal2(undefined);
886
+ const route = context.ui.router.current();
887
+ if (route.type === "session")
888
+ setSessionID(route.sessionID);
889
+ const insights = useSessionInsights(sessionID);
890
+ const current = () => insights.latest ?? EMPTY_INSIGHTS;
891
+ return /* @__PURE__ */ jsxDEV("box", {
892
+ flexDirection: "column",
893
+ padding: 1,
894
+ children: [
895
+ /* @__PURE__ */ jsxDEV("text", {
896
+ fg: themeColor(context.theme, ["text", "default"]),
897
+ children: "gvozd session insights"
898
+ }, undefined, false, undefined, this),
899
+ /* @__PURE__ */ jsxDEV(Show2, {
900
+ when: sessionID(),
901
+ fallback: /* @__PURE__ */ jsxDEV("text", {
902
+ fg: themeColor(context.theme, ["text", "muted"]),
903
+ children: "open inside a session to see insights"
904
+ }, undefined, false, undefined, this),
905
+ children: /* @__PURE__ */ jsxDEV("box", {
906
+ flexDirection: "column",
907
+ children: [
908
+ /* @__PURE__ */ jsxDEV(SubagentsSection, {
909
+ insights: current()
910
+ }, undefined, false, undefined, this),
911
+ /* @__PURE__ */ jsxDEV(SkillsSection, {
912
+ insights: current()
913
+ }, undefined, false, undefined, this),
914
+ /* @__PURE__ */ jsxDEV(PermissionsSection, {
915
+ insights: current()
916
+ }, undefined, false, undefined, this),
917
+ /* @__PURE__ */ jsxDEV(ToolsSection, {
918
+ insights: current()
919
+ }, undefined, false, undefined, this)
920
+ ]
921
+ }, undefined, true, undefined, this)
922
+ }, undefined, false, undefined, this)
923
+ ]
924
+ }, undefined, true, undefined, this);
925
+ }
926
+ function FooterStatusSlot(props) {
927
+ const insights = useSessionInsights(() => props.sessionID);
928
+ const current = () => insights.latest ?? EMPTY_INSIGHTS;
929
+ const context = usePlugin2();
930
+ const running = () => current().tree.filter((node) => !node.isRoot && node.status === "running").length;
931
+ const pending = () => current().permissions.filter((entry) => entry.pending).length;
932
+ const cost = () => current().tree.reduce((sum, node) => sum + node.cost, 0);
933
+ return /* @__PURE__ */ jsxDEV(Show2, {
934
+ when: running() > 0 || pending() > 0 || cost() > 0,
935
+ children: /* @__PURE__ */ jsxDEV("text", {
936
+ fg: themeColor(context.theme, ["text", "muted"]),
937
+ children: formatFooterStatus(current().permissions, current().tree)
938
+ }, undefined, false, undefined, this)
939
+ }, undefined, false, undefined, this);
940
+ }
941
+ function ModePanel() {
942
+ const context = usePlugin2();
943
+ const route = context.ui.router.current();
944
+ const sessionID = route.type === "session" ? route.sessionID : undefined;
945
+ const [applied, setApplied] = createSignal2();
946
+ const [busy, setBusy] = createSignal2(false);
947
+ const apply = async (mode) => {
948
+ if (!sessionID || busy())
949
+ return;
950
+ setBusy(true);
951
+ try {
952
+ const result = await setTrustMode(sessionID, mode);
953
+ if (result)
954
+ setApplied(result.mode);
955
+ } finally {
956
+ setBusy(false);
957
+ }
958
+ };
959
+ return /* @__PURE__ */ jsxDEV("box", {
960
+ flexDirection: "column",
961
+ padding: 1,
962
+ children: [
963
+ /* @__PURE__ */ jsxDEV("text", {
964
+ fg: themeColor(context.theme, ["text", "default"]),
965
+ children: "gvozd permission mode"
966
+ }, undefined, false, undefined, this),
967
+ /* @__PURE__ */ jsxDEV(Show2, {
968
+ when: sessionID,
969
+ fallback: /* @__PURE__ */ jsxDEV("text", {
970
+ fg: themeColor(context.theme, ["text", "muted"]),
971
+ children: "open inside a session to switch modes"
972
+ }, undefined, false, undefined, this),
973
+ children: /* @__PURE__ */ jsxDEV("box", {
974
+ flexDirection: "column",
975
+ children: [
976
+ /* @__PURE__ */ jsxDEV("text", {
977
+ fg: themeColor(context.theme, ["text", "default"]),
978
+ children: busy() ? "applying\u2026" : "select a posture (enter to apply):"
979
+ }, undefined, false, undefined, this),
980
+ /* @__PURE__ */ jsxDEV(For2, {
981
+ each: ["balanced", "trusted", "strict"],
982
+ children: (mode) => /* @__PURE__ */ jsxDEV("text", {
983
+ fg: themeColor(context.theme, ["text", "default"]),
984
+ children: `\u25B8 ${mode}: ${MODE_HINTS[mode]}`
985
+ }, undefined, false, undefined, this)
986
+ }, undefined, false, undefined, this),
987
+ /* @__PURE__ */ jsxDEV(Show2, {
988
+ when: applied(),
989
+ children: /* @__PURE__ */ jsxDEV("text", {
990
+ fg: themeColor(context.theme, ["status", "success"]),
991
+ children: `applied: ${applied()} \u2014 child sessions inherit it`
992
+ }, undefined, false, undefined, this)
993
+ }, undefined, false, undefined, this)
994
+ ]
995
+ }, undefined, true, undefined, this)
996
+ }, undefined, false, undefined, this)
997
+ ]
998
+ }, undefined, true, undefined, this);
999
+ }
1000
+ var MODE_HINTS = {
1001
+ balanced: "ask for unknown shell and edits (current default)",
1002
+ trusted: "allow all shell and edits; destructive git still denied",
1003
+ strict: "ask for every shell command and edit"
1004
+ };
1005
+ function KeymapCommands() {
1006
+ const context = usePlugin2();
1007
+ context.keymap.layer(() => ({
1008
+ mode: "global",
1009
+ commands: GVOZD_COMMANDS.map((command) => ({
1010
+ id: command.id,
1011
+ title: command.title,
1012
+ group: "Gvozd",
1013
+ palette: true,
1014
+ slash: { name: command.slash },
1015
+ run: () => {
1016
+ context.ui.panel.open(command.panel, { presentation: "fullscreen" });
1017
+ }
1018
+ }))
1019
+ }));
1020
+ return null;
1021
+ }
1022
+ var GVOZD_COMMANDS = [
1023
+ { id: "gvozd.insights", title: "Gvozd session insights", panel: "gvozd.insights", slash: "gvozd" },
1024
+ { id: "gvozd.dryrun", title: "Gvozd permission dry-run", panel: "gvozd.dryrun", slash: "gvozd-dryrun" },
1025
+ { id: "gvozd.leases", title: "Gvozd file leases", panel: "gvozd.leases", slash: "gvozd-leases" },
1026
+ { id: "gvozd.mode", title: "Gvozd permission mode", panel: "gvozd.mode", slash: "gvozd-mode" }
1027
+ ];
1028
+ var tui_default = Plugin.define({
1029
+ id: "agent-gvozd",
1030
+ setup(context) {
1031
+ const unregisterSidebar = context.ui.slot({
1032
+ append: "sidebar.content",
1033
+ render: ({ sessionID }) => /* @__PURE__ */ jsxDEV(SessionInsightsSlot, {
1034
+ sessionID
1035
+ }, undefined, false, undefined, this)
1036
+ });
1037
+ const unregisterFooter = context.ui.slot({
1038
+ append: "prompt.footer.status",
1039
+ render: ({ sessionID }) => sessionID ? /* @__PURE__ */ jsxDEV(FooterStatusSlot, {
1040
+ sessionID
1041
+ }, undefined, false, undefined, this) : null
1042
+ });
1043
+ const unregisterKeymapHost = context.ui.slot({
1044
+ append: "app",
1045
+ render: () => /* @__PURE__ */ jsxDEV(KeymapCommands, {}, undefined, false, undefined, this)
1046
+ });
1047
+ const unregisterPanelSlot = context.ui.slot({
1048
+ append: "session.panel",
1049
+ render: (panel) => /* @__PURE__ */ jsxDEV(Fragment, {
1050
+ children: [
1051
+ /* @__PURE__ */ jsxDEV(Show2, {
1052
+ when: panel.name === "gvozd.insights",
1053
+ children: /* @__PURE__ */ jsxDEV(FullscreenPanel, {}, undefined, false, undefined, this)
1054
+ }, undefined, false, undefined, this),
1055
+ /* @__PURE__ */ jsxDEV(Show2, {
1056
+ when: panel.name === "gvozd.dryrun",
1057
+ children: /* @__PURE__ */ jsxDEV(DryRunPanel, {}, undefined, false, undefined, this)
1058
+ }, undefined, false, undefined, this),
1059
+ /* @__PURE__ */ jsxDEV(Show2, {
1060
+ when: panel.name === "gvozd.leases",
1061
+ children: /* @__PURE__ */ jsxDEV(LeasePanel, {}, undefined, false, undefined, this)
1062
+ }, undefined, false, undefined, this),
1063
+ /* @__PURE__ */ jsxDEV(Show2, {
1064
+ when: panel.name === "gvozd.mode",
1065
+ children: /* @__PURE__ */ jsxDEV(ModePanel, {}, undefined, false, undefined, this)
1066
+ }, undefined, false, undefined, this)
1067
+ ]
1068
+ }, undefined, true, undefined, this)
1069
+ });
1070
+ return () => {
1071
+ unregisterSidebar();
1072
+ unregisterFooter();
1073
+ unregisterPanelSlot();
1074
+ unregisterKeymapHost();
1075
+ };
1076
+ }
1077
+ });
1078
+ export {
1079
+ tui_default as default
1080
+ };