@nail00749/agent-gvozd 0.1.8 → 0.2.0

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