kibi-opencode 0.10.0 → 0.12.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.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { TuiPlugin } from "@opencode-ai/plugin/tui";
2
+ declare const tui: TuiPlugin;
3
+ export { tui };
4
+ declare const _default: {
5
+ readonly id: "kibi-opencode";
6
+ readonly tui: TuiPlugin;
7
+ };
8
+ export default _default;
package/dist/tui.js ADDED
@@ -0,0 +1,413 @@
1
+ // @bun
2
+ // src/idle-brief-reader.ts
3
+ import * as fs from "fs";
4
+ import * as path2 from "path";
5
+
6
+ // src/idle-brief-paths.ts
7
+ import * as path from "path";
8
+ import { loadBriefConfig } from "kibi-cli/brief-config";
9
+ function resolveBriefsDir(workspaceRoot) {
10
+ return path.join(workspaceRoot, ".kb", "briefs");
11
+ }
12
+
13
+ // src/idle-brief-store.ts
14
+ function isRecord(value) {
15
+ return typeof value === "object" && value !== null;
16
+ }
17
+ function isStringArray(value) {
18
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
19
+ }
20
+ function isCitation(value) {
21
+ return isRecord(value) && typeof value.id === "string";
22
+ }
23
+ function isStatement(value) {
24
+ return isRecord(value) && typeof value.statement === "string" && isStringArray(value.citationIds);
25
+ }
26
+ function isValidationViolation(value) {
27
+ return isRecord(value) && typeof value.rule === "string" && typeof value.entityId === "string" && typeof value.description === "string";
28
+ }
29
+ function isValidationDiagnostic(value) {
30
+ return isRecord(value) && typeof value.category === "string" && typeof value.severity === "string" && typeof value.message === "string";
31
+ }
32
+ function isAuditCursor(value) {
33
+ return isRecord(value) && typeof value.lastTimestamp === "string" && typeof value.lastOperation === "string" && typeof value.entryCount === "number" && typeof value.fileSize === "number";
34
+ }
35
+ function isValidation(value) {
36
+ return isRecord(value) && Array.isArray(value.violations) && value.violations.every(isValidationViolation) && typeof value.count === "number" && Array.isArray(value.diagnostics) && value.diagnostics.every(isValidationDiagnostic);
37
+ }
38
+ function isBriefingBase(value) {
39
+ return isRecord(value) && typeof value.tldr === "string" && typeof value.promptBlock === "string" && Array.isArray(value.citations) && value.citations.every(isCitation) && (value.constraints === undefined || Array.isArray(value.constraints) && value.constraints.every(isStatement)) && (value.regressionRisks === undefined || Array.isArray(value.regressionRisks) && value.regressionRisks.every(isStatement)) && (value.missingEvidence === undefined || Array.isArray(value.missingEvidence) && value.missingEvidence.every(isStatement));
40
+ }
41
+ function isBriefingV2(value) {
42
+ return isBriefingBase(value) && isStringArray(value.changeNarrative) && (value.deliveryReasons === undefined || isDeliveryReasons(value.deliveryReasons));
43
+ }
44
+ function isReasonItem(value) {
45
+ return isRecord(value) && typeof value.kind === "string" && typeof value.text === "string" && isStringArray(value.entityIds) && (value.citationIds === undefined || isStringArray(value.citationIds)) && (value.severity === undefined || value.severity === "info" || value.severity === "warning" || value.severity === "error");
46
+ }
47
+ function isDeliveryReasons(value) {
48
+ return isRecord(value) && value.version === 1 && isRecord(value.toast) && typeof value.toast.title === "string" && typeof value.toast.summary === "string" && typeof value.toast.whyItMatters === "string" && Array.isArray(value.items) && value.items.every(isReasonItem);
49
+ }
50
+ function isChangeItem(value) {
51
+ return isRecord(value) && typeof value.id === "string" && typeof value.type === "string";
52
+ }
53
+ function isIdleBriefEnvelope(value) {
54
+ if (!isRecord(value))
55
+ return false;
56
+ const hasBaseFields = (value.schemaVersion === "1.0" || value.schemaVersion === "2.0") && typeof value.briefId === "string" && (value.type === "success" || value.type === "warning") && typeof value.sessionId === "string" && typeof value.branch === "string" && typeof value.createdAt === "string" && typeof value.unread === "boolean" && isAuditCursor(value.auditCursor) && typeof value.summary === "string" && isValidation(value.validation) && typeof value.contentHash === "string";
57
+ if (!hasBaseFields)
58
+ return false;
59
+ if (value.schemaVersion === "1.0") {
60
+ return isRecord(value.counts) && typeof value.counts.requirementsAdded === "number" && typeof value.counts.relationshipsAdded === "number" && typeof value.counts.entitiesDeleted === "number" && isBriefingBase(value.briefing);
61
+ }
62
+ return isRecord(value.counts) && typeof value.counts.entitiesAdded === "number" && typeof value.counts.entitiesModified === "number" && typeof value.counts.entitiesRemoved === "number" && typeof value.counts.relationshipsChanged === "number" && isRecord(value.changes) && isRecord(value.changes.entities) && Array.isArray(value.changes.entities.added) && value.changes.entities.added.every(isChangeItem) && Array.isArray(value.changes.entities.modified) && value.changes.entities.modified.every(isChangeItem) && Array.isArray(value.changes.entities.removed) && value.changes.entities.removed.every(isChangeItem) && isRecord(value.changes.relationships) && typeof value.changes.relationships.changed === "number" && isBriefingV2(value.briefing);
63
+ }
64
+
65
+ // src/idle-brief-reader.ts
66
+ var BRIEF_FILENAME_RE = /^(\d+)_brief\.json$/;
67
+ var TUI_SEEN_FILE = ".tui-seen.json";
68
+ function resolveTuiSeenPath(workspaceRoot) {
69
+ return path2.join(resolveBriefsDir(workspaceRoot), TUI_SEEN_FILE);
70
+ }
71
+ function markBriefTuiSeen(workspaceRoot, branch, contentHash) {
72
+ const briefsDir = resolveBriefsDir(workspaceRoot);
73
+ fs.mkdirSync(briefsDir, { recursive: true });
74
+ const seenPath = resolveTuiSeenPath(workspaceRoot);
75
+ let parsed = {};
76
+ try {
77
+ const raw = JSON.parse(fs.readFileSync(seenPath, "utf-8"));
78
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
79
+ parsed = raw;
80
+ }
81
+ } catch {}
82
+ const existing = Array.isArray(parsed[branch]) ? parsed[branch] : [];
83
+ const next = [contentHash, ...existing.filter((entry) => entry !== contentHash)].slice(0, 100);
84
+ parsed[branch] = next;
85
+ const tempPath = `${seenPath}.tmp`;
86
+ fs.writeFileSync(tempPath, JSON.stringify(parsed, null, 2), "utf-8");
87
+ fs.renameSync(tempPath, seenPath);
88
+ }
89
+ function extractTimestamp(filename) {
90
+ const match = filename.match(BRIEF_FILENAME_RE);
91
+ if (!match)
92
+ return null;
93
+ return Number(match[1]);
94
+ }
95
+ function scanBriefs(workspaceRoot, branch, filterUnread) {
96
+ const briefsDir = resolveBriefsDir(workspaceRoot);
97
+ if (!fs.existsSync(briefsDir)) {
98
+ return null;
99
+ }
100
+ const files = fs.readdirSync(briefsDir);
101
+ const candidates = [];
102
+ for (const file of files) {
103
+ if (file.endsWith(".tmp"))
104
+ continue;
105
+ const timestamp = extractTimestamp(file);
106
+ if (timestamp === null)
107
+ continue;
108
+ const filePath = path2.join(briefsDir, file);
109
+ let envelope;
110
+ try {
111
+ const raw = fs.readFileSync(filePath, "utf-8");
112
+ const parsed = JSON.parse(raw);
113
+ if (!isIdleBriefEnvelope(parsed)) {
114
+ continue;
115
+ }
116
+ envelope = parsed;
117
+ } catch {
118
+ continue;
119
+ }
120
+ if (envelope.branch === branch && (envelope.schemaVersion === "1.0" || envelope.schemaVersion === "2.0") && (!filterUnread || envelope.unread === true)) {
121
+ candidates.push({ timestamp, envelope, filePath });
122
+ }
123
+ }
124
+ if (candidates.length === 0) {
125
+ return null;
126
+ }
127
+ candidates.sort((a, b) => b.timestamp - a.timestamp);
128
+ const latest = candidates[0];
129
+ if (!latest) {
130
+ return null;
131
+ }
132
+ return {
133
+ envelope: latest.envelope,
134
+ filePath: latest.filePath
135
+ };
136
+ }
137
+ function selectLatestPersistedBrief(workspaceRoot, branch) {
138
+ return scanBriefs(workspaceRoot, branch, false);
139
+ }
140
+ function markBriefRead(workspaceRoot, briefPath) {
141
+ const briefsDir = resolveBriefsDir(workspaceRoot);
142
+ const resolvedBriefPath = path2.resolve(briefPath);
143
+ const resolvedBriefsDir = path2.resolve(briefsDir);
144
+ if (!resolvedBriefPath.startsWith(resolvedBriefsDir + path2.sep)) {
145
+ throw new Error(`Invalid brief path: ${briefPath} is not inside ${briefsDir}`);
146
+ }
147
+ const raw = fs.readFileSync(briefPath, "utf-8");
148
+ const brief = JSON.parse(raw);
149
+ brief.unread = false;
150
+ const tempPath = `${briefPath}.tmp`;
151
+ fs.writeFileSync(tempPath, JSON.stringify(brief, null, 2), "utf-8");
152
+ fs.renameSync(tempPath, briefPath);
153
+ }
154
+
155
+ // src/tui.tsx
156
+ import { loadBriefConfig as loadBriefConfig2 } from "kibi-cli/brief-config";
157
+
158
+ // src/brief-delivery-reasons.ts
159
+ function toastSummary(items) {
160
+ const first = items[0]?.text?.trim() ?? "";
161
+ const second = items[1]?.text?.trim() ?? "";
162
+ if (first && second)
163
+ return `${first}, ${second}`;
164
+ return first || second || undefined;
165
+ }
166
+ function toastWhy(items) {
167
+ if (items.some((i) => i.kind === "conflict_detected"))
168
+ return "There is a knowledge conflict to resolve before using the brief.";
169
+ if (items.some((i) => i.kind === "validation_issue"))
170
+ return "Validation issues need attention before the update is treated as settled.";
171
+ const hasEntities = items.some((i) => i.kind === "entity_added" || i.kind === "entity_modified" || i.kind === "entity_removed");
172
+ const hasRelationships = items.some((i) => i.kind === "relationship_changed");
173
+ if (hasEntities && hasRelationships)
174
+ return "Requirements and facts were updated.";
175
+ if (hasEntities)
176
+ return "Entities were updated.";
177
+ if (hasRelationships)
178
+ return "Relationships were updated.";
179
+ return;
180
+ }
181
+ function isOperationalByEntityIds(item) {
182
+ if (item.entityIds.length === 0)
183
+ return false;
184
+ return item.entityIds.every((id) => {
185
+ const dashIdx = id.indexOf("-");
186
+ if (dashIdx < 0)
187
+ return false;
188
+ const name = id.slice(dashIdx + 1);
189
+ return /\.[a-zA-Z0-9]+$/.test(name);
190
+ });
191
+ }
192
+ function isOperationalItem(item, allItems) {
193
+ if (item.kind === "relationship_changed") {
194
+ const entityItems = allItems.filter((i) => i.kind === "entity_added" || i.kind === "entity_modified" || i.kind === "entity_removed");
195
+ return entityItems.length > 0 && entityItems.every(isOperationalByEntityIds);
196
+ }
197
+ return isOperationalByEntityIds(item);
198
+ }
199
+ function renderToastSummary(reasons) {
200
+ const domainItems = reasons.items.filter((i) => !isOperationalItem(i, reasons.items));
201
+ if (domainItems.length === 0) {
202
+ return;
203
+ }
204
+ return {
205
+ title: "Kibi Knowledge Update",
206
+ summary: toastSummary(domainItems) ?? "",
207
+ whyItMatters: toastWhy(domainItems) ?? ""
208
+ };
209
+ }
210
+
211
+ // src/tui-brief-view-model.ts
212
+ function firstNonEmpty(...values) {
213
+ for (const value of values) {
214
+ const trimmed = value?.trim();
215
+ if (trimmed) {
216
+ return trimmed;
217
+ }
218
+ }
219
+ return;
220
+ }
221
+ function isOperationalByEntityIds2(item) {
222
+ if (item.entityIds.length === 0)
223
+ return false;
224
+ return item.entityIds.every((id) => {
225
+ const dashIdx = id.indexOf("-");
226
+ if (dashIdx < 0)
227
+ return false;
228
+ return /\.[a-zA-Z0-9]+$/.test(id.slice(dashIdx + 1));
229
+ });
230
+ }
231
+ function isOperationalDeliveryItem(item, allItems) {
232
+ if (item.kind === "relationship_changed") {
233
+ const entityItems = allItems.filter((i) => i.kind === "entity_added" || i.kind === "entity_modified" || i.kind === "entity_removed");
234
+ return entityItems.length > 0 && entityItems.every(isOperationalByEntityIds2);
235
+ }
236
+ return isOperationalByEntityIds2(item);
237
+ }
238
+ function deriveWhatChanged(envelope) {
239
+ const briefing = envelope.briefing;
240
+ const deliveryReasons = briefing.deliveryReasons;
241
+ if (deliveryReasons?.items?.length) {
242
+ const domainItems = deliveryReasons.items.filter((item) => !isOperationalDeliveryItem(item, deliveryReasons.items));
243
+ if (domainItems.length > 0) {
244
+ return domainItems.map((item) => item.text);
245
+ }
246
+ }
247
+ if (envelope.schemaVersion === "2.0") {
248
+ const narrative = envelope.briefing.changeNarrative.map((line) => line.trim()).filter(Boolean).filter((line) => !line.includes(".sisyphus/"));
249
+ if (narrative.length > 0) {
250
+ return narrative.slice(0, 2);
251
+ }
252
+ const fallbackEntity = envelope.changes.entities.modified[0] ?? envelope.changes.entities.added[0];
253
+ if (fallbackEntity) {
254
+ const action = envelope.changes.entities.modified[0] ? "Modified" : "Added";
255
+ return [
256
+ `${action} ${fallbackEntity.id}: ${fallbackEntity.title ?? "Untitled"}`
257
+ ];
258
+ }
259
+ }
260
+ const fallback = firstNonEmpty(envelope.summary, envelope.briefing.tldr);
261
+ return fallback ? [fallback] : [];
262
+ }
263
+ function buildTuiBriefViewModel(envelope) {
264
+ const briefing = envelope.briefing;
265
+ const deliveryReasons = briefing.deliveryReasons;
266
+ let title = firstNonEmpty(envelope.summary, envelope.briefing.tldr);
267
+ if (deliveryReasons?.items?.length) {
268
+ const filteredToast = renderToastSummary(deliveryReasons);
269
+ if (filteredToast) {
270
+ title = filteredToast.summary;
271
+ }
272
+ } else if (envelope.schemaVersion === "2.0" && envelope.briefing.changeNarrative.length > 0) {
273
+ title = envelope.briefing.changeNarrative[0]?.trim() ?? title;
274
+ }
275
+ const base = {
276
+ briefId: envelope.briefId,
277
+ schemaVersion: envelope.schemaVersion,
278
+ branch: envelope.branch,
279
+ createdAt: envelope.createdAt,
280
+ type: envelope.type,
281
+ unread: envelope.unread,
282
+ contentHash: envelope.contentHash,
283
+ title: title ?? "Kibi Brief",
284
+ whatChanged: deriveWhatChanged(envelope),
285
+ whyItMatters: deliveryReasons?.items?.length ? renderToastSummary(deliveryReasons)?.whyItMatters || undefined : undefined,
286
+ knowledgeImpact: {
287
+ citations: envelope.briefing.citations,
288
+ constraints: envelope.briefing.constraints ?? [],
289
+ regressionRisks: envelope.briefing.regressionRisks ?? []
290
+ },
291
+ interpretationNote: {
292
+ validationCount: envelope.validation.count,
293
+ missingEvidence: envelope.briefing.missingEvidence ?? []
294
+ }
295
+ };
296
+ if (envelope.schemaVersion === "2.0") {
297
+ return {
298
+ ...base,
299
+ counts: {
300
+ schemaVersion: "2.0",
301
+ entitiesAdded: envelope.counts.entitiesAdded,
302
+ entitiesModified: envelope.counts.entitiesModified,
303
+ entitiesRemoved: envelope.counts.entitiesRemoved,
304
+ relationshipsChanged: envelope.counts.relationshipsChanged
305
+ }
306
+ };
307
+ }
308
+ return {
309
+ ...base,
310
+ counts: {
311
+ schemaVersion: "1.0",
312
+ requirementsAdded: envelope.counts.requirementsAdded,
313
+ relationshipsAdded: envelope.counts.relationshipsAdded,
314
+ entitiesDeleted: envelope.counts.entitiesDeleted
315
+ }
316
+ };
317
+ }
318
+
319
+ // src/tui.tsx
320
+ var tui = async (api, _options, _meta) => {
321
+ let currentContentHash = null;
322
+ api.route.register([
323
+ {
324
+ name: "kibi.brief",
325
+ render: () => {
326
+ const workspace = api.state.path.worktree || "";
327
+ const branch = api.state.vcs?.branch || "main";
328
+ const brief = selectLatestPersistedBrief(workspace, branch);
329
+ if (!brief) {
330
+ currentContentHash = null;
331
+ return /* @__PURE__ */ h("box", {
332
+ flexDirection: "column",
333
+ gap: 1,
334
+ padding: 1
335
+ }, /* @__PURE__ */ h("text", {
336
+ fg: api.theme.current.error
337
+ }, "No meaningful KB update"), /* @__PURE__ */ h("text", null, "There is no latest persisted brief for this branch."));
338
+ }
339
+ const { envelope } = brief;
340
+ const isNewContent = envelope.contentHash !== currentContentHash;
341
+ currentContentHash = envelope.contentHash;
342
+ if (isNewContent && envelope.unread) {
343
+ markBriefTuiSeen(workspace, branch, envelope.contentHash);
344
+ try {
345
+ const config = loadBriefConfig2(workspace);
346
+ if (!config.channels.vscode) {
347
+ markBriefRead(workspace, brief.filePath);
348
+ }
349
+ } catch {}
350
+ }
351
+ const viewModel = buildTuiBriefViewModel(envelope);
352
+ return /* @__PURE__ */ h("scrollbox", {
353
+ flexDirection: "column",
354
+ gap: 1,
355
+ padding: 1
356
+ }, /* @__PURE__ */ h("text", {
357
+ fg: api.theme.current.accent
358
+ }, /* @__PURE__ */ h("strong", null, viewModel.title)), /* @__PURE__ */ h("box", {
359
+ flexDirection: "column"
360
+ }, /* @__PURE__ */ h("text", null, /* @__PURE__ */ h("strong", null, "What changed:")), /* @__PURE__ */ h("text", null, viewModel.whatChanged.map((line) => `- ${line}`).join(`
361
+ `))), /* @__PURE__ */ h("box", {
362
+ flexDirection: "column"
363
+ }, /* @__PURE__ */ h("text", null, /* @__PURE__ */ h("strong", null, "Why it matters:")), /* @__PURE__ */ h("text", null, viewModel.whyItMatters)), (viewModel.knowledgeImpact.citations.length > 0 || viewModel.knowledgeImpact.constraints.length > 0 || viewModel.knowledgeImpact.regressionRisks.length > 0) && /* @__PURE__ */ h("box", {
364
+ flexDirection: "column"
365
+ }, /* @__PURE__ */ h("text", null, /* @__PURE__ */ h("strong", null, "Project knowledge impact:")), viewModel.knowledgeImpact.citations.length > 0 && /* @__PURE__ */ h("text", null, viewModel.knowledgeImpact.citations.map((citation) => `- ${citation.id}${citation.title ? `: ${citation.title}` : ""}`).join(`
366
+ `)), viewModel.knowledgeImpact.constraints.length > 0 && /* @__PURE__ */ h("text", null, viewModel.knowledgeImpact.constraints.map((constraint) => `- ${constraint.statement}`).join(`
367
+ `)), viewModel.knowledgeImpact.regressionRisks.length > 0 && /* @__PURE__ */ h("text", null, viewModel.knowledgeImpact.regressionRisks.map((risk) => `- ${risk.statement}`).join(`
368
+ `))), (viewModel.interpretationNote.validationCount > 0 || viewModel.interpretationNote.missingEvidence.length > 0) && /* @__PURE__ */ h("box", {
369
+ flexDirection: "column"
370
+ }, /* @__PURE__ */ h("text", {
371
+ fg: api.theme.current.warning
372
+ }, /* @__PURE__ */ h("strong", null, "Interpretation note:")), viewModel.interpretationNote.validationCount > 0 && /* @__PURE__ */ h("text", null, "Validation checks reported unresolved items: ", viewModel.interpretationNote.validationCount, " issue(s)."), viewModel.interpretationNote.missingEvidence.length > 0 && /* @__PURE__ */ h("text", null, viewModel.interpretationNote.missingEvidence.map((item) => `- ${item.statement}`).join(`
373
+ `))));
374
+ }
375
+ }
376
+ ]);
377
+ if (api.command?.register) {
378
+ api.command.register(() => [
379
+ {
380
+ title: "Kibi: Open Latest Brief",
381
+ value: "kibi.open_latest_brief",
382
+ description: "Opens the latest persisted brief for the current workspace and branch",
383
+ onSelect: () => {
384
+ api.route.navigate("kibi.brief");
385
+ }
386
+ },
387
+ {
388
+ title: "Kibi: Open Latest Brief",
389
+ value: "kibi-brief",
390
+ description: "Opens the latest persisted brief for the current workspace and branch",
391
+ onSelect: () => {
392
+ api.route.navigate("kibi.brief");
393
+ }
394
+ },
395
+ {
396
+ title: "Kibi: Refresh Brief",
397
+ value: "kibi.refresh_brief",
398
+ description: "Re-reads the latest persisted brief and refreshes the view",
399
+ onSelect: () => {
400
+ api.route.navigate("kibi.brief");
401
+ }
402
+ }
403
+ ]);
404
+ }
405
+ };
406
+ var tui_default = {
407
+ id: "kibi-opencode",
408
+ tui
409
+ };
410
+ export {
411
+ tui,
412
+ tui_default as default
413
+ };
package/dist/tui.jsx ADDED
@@ -0,0 +1,120 @@
1
+ import { selectLatestPersistedBrief, markBriefTuiSeen, markBriefRead } from "./idle-brief-reader.js";
2
+ import { loadBriefConfig } from "kibi-cli/brief-config";
3
+ import { buildTuiBriefViewModel } from "./tui-brief-view-model.js";
4
+ const tui = async (api, _options, _meta) => {
5
+ // State: track the currently displayed contentHash for in-place refresh detection
6
+ let currentContentHash = null;
7
+ api.route.register([
8
+ {
9
+ name: "kibi.brief",
10
+ render: () => {
11
+ const workspace = api.state.path.worktree || "";
12
+ const branch = api.state.vcs?.branch || "main";
13
+ const brief = selectLatestPersistedBrief(workspace, branch);
14
+ if (!brief) {
15
+ currentContentHash = null;
16
+ return (<box flexDirection="column" gap={1} padding={1}>
17
+ <text fg={api.theme.current.error}>No meaningful KB update</text>
18
+ <text>There is no latest persisted brief for this branch.</text>
19
+ </box>);
20
+ }
21
+ const { envelope } = brief;
22
+ const isNewContent = envelope.contentHash !== currentContentHash;
23
+ currentContentHash = envelope.contentHash;
24
+ // Mark as TUI-seen when this is a new (previously unseen) brief and it's unread
25
+ if (isNewContent && envelope.unread) {
26
+ markBriefTuiSeen(workspace, branch, envelope.contentHash);
27
+ // When VSCode channel is disabled, TUI is the sole delivery channel —
28
+ // viewing the brief here should also mark it as fully read
29
+ try {
30
+ const config = loadBriefConfig(workspace);
31
+ if (!config.channels.vscode) {
32
+ markBriefRead(workspace, brief.filePath);
33
+ }
34
+ }
35
+ catch {
36
+ // Gracefully handle config load or markBriefRead failures
37
+ }
38
+ }
39
+ const viewModel = buildTuiBriefViewModel(envelope);
40
+ return (<scrollbox flexDirection="column" gap={1} padding={1}>
41
+ <text fg={api.theme.current.accent}><strong>{viewModel.title}</strong></text>
42
+
43
+ <box flexDirection="column">
44
+ <text><strong>What changed:</strong></text>
45
+ <text>{viewModel.whatChanged.map((line) => `- ${line}`).join("\n")}</text>
46
+ </box>
47
+
48
+ <box flexDirection="column">
49
+ <text><strong>Why it matters:</strong></text>
50
+ <text>{viewModel.whyItMatters}</text>
51
+ </box>
52
+
53
+ {(viewModel.knowledgeImpact.citations.length > 0 || viewModel.knowledgeImpact.constraints.length > 0 || viewModel.knowledgeImpact.regressionRisks.length > 0) && (<box flexDirection="column">
54
+ <text><strong>Project knowledge impact:</strong></text>
55
+ {viewModel.knowledgeImpact.citations.length > 0 && (<text>
56
+ {viewModel.knowledgeImpact.citations
57
+ .map((citation) => `- ${citation.id}${citation.title ? `: ${citation.title}` : ""}`)
58
+ .join("\n")}
59
+ </text>)}
60
+ {viewModel.knowledgeImpact.constraints.length > 0 && (<text>
61
+ {viewModel.knowledgeImpact.constraints
62
+ .map((constraint) => `- ${constraint.statement}`)
63
+ .join("\n")}
64
+ </text>)}
65
+ {viewModel.knowledgeImpact.regressionRisks.length > 0 && (<text>
66
+ {viewModel.knowledgeImpact.regressionRisks
67
+ .map((risk) => `- ${risk.statement}`)
68
+ .join("\n")}
69
+ </text>)}
70
+ </box>)}
71
+
72
+ {(viewModel.interpretationNote.validationCount > 0 || viewModel.interpretationNote.missingEvidence.length > 0) && (<box flexDirection="column">
73
+ <text fg={api.theme.current.warning}><strong>Interpretation note:</strong></text>
74
+ {viewModel.interpretationNote.validationCount > 0 && (<text>Validation checks reported unresolved items: {viewModel.interpretationNote.validationCount} issue(s).</text>)}
75
+ {viewModel.interpretationNote.missingEvidence.length > 0 && (<text>
76
+ {viewModel.interpretationNote.missingEvidence
77
+ .map((item) => `- ${item.statement}`)
78
+ .join("\n")}
79
+ </text>)}
80
+ </box>)}
81
+ </scrollbox>);
82
+ },
83
+ },
84
+ ]);
85
+ if (api.command?.register) {
86
+ api.command.register(() => [
87
+ {
88
+ title: "Kibi: Open Latest Brief",
89
+ value: "kibi.open_latest_brief",
90
+ description: "Opens the latest persisted brief for the current workspace and branch",
91
+ // implements REQ-opencode-kibi-briefing-v6
92
+ onSelect: () => {
93
+ api.route.navigate("kibi.brief");
94
+ },
95
+ },
96
+ {
97
+ title: "Kibi: Open Latest Brief",
98
+ value: "kibi-brief",
99
+ description: "Opens the latest persisted brief for the current workspace and branch",
100
+ // implements REQ-opencode-kibi-briefing-v6
101
+ onSelect: () => {
102
+ api.route.navigate("kibi.brief");
103
+ },
104
+ },
105
+ {
106
+ title: "Kibi: Refresh Brief",
107
+ value: "kibi.refresh_brief",
108
+ description: "Re-reads the latest persisted brief and refreshes the view",
109
+ onSelect: () => {
110
+ api.route.navigate("kibi.brief");
111
+ },
112
+ },
113
+ ]);
114
+ }
115
+ };
116
+ export { tui };
117
+ export default {
118
+ id: "kibi-opencode",
119
+ tui,
120
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-opencode",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Kibi OpenCode plugin - thin adapter to integrate Kibi with OpenCode sessions",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -30,6 +30,11 @@
30
30
  "types": "./dist/file-filter.d.ts",
31
31
  "import": "./dist/file-filter.js",
32
32
  "default": "./dist/file-filter.js"
33
+ },
34
+ "./tui": {
35
+ "types": "./dist/tui.d.ts",
36
+ "import": "./dist/tui.js",
37
+ "default": "./dist/tui.js"
33
38
  }
34
39
  },
35
40
  "files": [
@@ -47,14 +52,17 @@
47
52
  "access": "public"
48
53
  },
49
54
  "scripts": {
50
- "build": "tsc -p tsconfig.json",
55
+ "build": "tsc -p tsconfig.json && bun run scripts/build-tui.ts && tsc -p tsconfig.tui-types.json",
56
+ "build:tui": "bun run scripts/build-tui.ts && tsc -p tsconfig.tui-types.json",
51
57
  "dev": "tsc -p tsconfig.json --watch",
52
58
  "clean": "rm -rf dist",
53
59
  "prepack": "npm run build"
54
60
  },
55
61
  "dependencies": {
56
- "@opencode-ai/plugin": "^1.2.26",
57
- "kibi-cli": "^0.7.0"
62
+ "@opencode-ai/plugin": "^1.4.7",
63
+ "@opentui/core": "^0.1.99",
64
+ "@opentui/solid": "^0.1.99",
65
+ "kibi-cli": "^0.10.0"
58
66
  },
59
67
  "devDependencies": {
60
68
  "@types/node": "latest",