@litfamily/litopencode 1.0.17 → 1.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README-Ko-KR.md +56 -86
- package/README.md +57 -86
- package/dist/activation-routing.js +3 -1
- package/dist/cli/args.js +5 -1
- package/dist/cli/auto-update.js +1 -1
- package/dist/cli/doctor.js +7 -5
- package/dist/cli/host-limits.js +2 -5
- package/dist/cli/install-config.d.ts +4 -0
- package/dist/cli/install-config.js +27 -1
- package/dist/cli/install-report.js +4 -1
- package/dist/cli/install.js +89 -28
- package/dist/cli/json.d.ts +1 -0
- package/dist/cli/json.js +10 -4
- package/dist/cli/jsonc.d.ts +17 -0
- package/dist/cli/jsonc.js +174 -0
- package/dist/cli/loop.d.ts +1 -1
- package/dist/cli/loop.js +81 -1
- package/dist/cli/motion-runtime.d.ts +13 -1
- package/dist/cli/motion-runtime.js +89 -2
- package/dist/cli/plugin-mutation.js +11 -6
- package/dist/cli/tui-config.d.ts +52 -0
- package/dist/cli/tui-config.js +198 -0
- package/dist/cli/types.d.ts +4 -0
- package/dist/cli.js +8 -3
- package/dist/deliverable-hedge-guard.js +5 -3
- package/dist/hooks.js +2 -1
- package/dist/tui/mission-band.d.ts +29 -0
- package/dist/tui/mission-band.js +116 -0
- package/dist/tui/mission-model.d.ts +139 -0
- package/dist/tui/mission-model.js +318 -0
- package/dist/tui/mission-motion.d.ts +66 -0
- package/dist/tui/mission-motion.js +147 -0
- package/dist/tui/mission-source.d.ts +56 -0
- package/dist/tui/mission-source.js +88 -0
- package/dist/tui/mission-text.d.ts +103 -0
- package/dist/tui/mission-text.js +613 -0
- package/dist/tui.d.ts +6 -0
- package/dist/tui.js +317 -0
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/reference-Ko-KR.md +72 -1
- package/docs/reference.md +82 -1
- package/package.json +21 -2
- package/skills/lit-docx/scripts/docx_design.py +1 -1
- package/skills/lit-docx/templates/tonalities/brief.yaml +0 -1
- package/skills/lit-docx/templates/tonalities/journal.yaml +0 -1
- package/skills/lit-docx/templates/tonalities/manual.yaml +0 -1
- package/skills/lit-docx/templates/tonalities/memo.yaml +0 -1
- package/skills/lit-docx/templates/tonalities/proposal.yaml +0 -1
- package/skills/lit-docx/templates/tonalities/report.yaml +0 -1
- package/skills/lit-pptx/references/tonalities/atlas.md +3 -4
- package/skills/lit-pptx/references/tonalities/chalk.md +2 -2
- package/skills/lit-pptx/references/tonalities/gazette.md +3 -3
- package/skills/lit-pptx/references/tonalities/ledger.md +6 -7
- package/skills/lit-pptx/references/tonalities/night.md +3 -3
- package/skills/lit-pptx/references/tonalities/paper.md +4 -5
- package/skills/lit-pptx/references/tonalities/signal.md +3 -4
- package/skills/lit-pptx/references/tonalities/studio.md +3 -3
- package/skills/lit-pptx/scripts/lib/template-registry.js +1 -1
- package/skills/lit-pptx/scripts/validate_pptx.py +10 -4
- package/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +0 -12
- package/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +0 -12
- package/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +0 -12
- package/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +2 -14
- package/skills/lit-pptx/templates/tonalities/night/pack.yaml +0 -12
- package/skills/lit-pptx/templates/tonalities/paper/pack.yaml +0 -12
- package/skills/lit-pptx/templates/tonalities/signal/pack.yaml +0 -12
- package/skills/lit-pptx/templates/tonalities/studio/pack.yaml +0 -12
- package/skills/managed-skill-manifest.json +25 -25
- package/tools/check-pack-payload.mjs +20 -5
- package/tools/version-manifests.json +18 -6
package/dist/tui.js
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { createEffect, createMemo, createSignal } from "solid-js";
|
|
2
|
+
import { useTerminalDimensions } from "@opentui/solid";
|
|
3
|
+
import { jsx } from "@opentui/solid/jsx-runtime";
|
|
4
|
+
import { buildMissionView, lastMission, leadingNotice, missionNotices } from "./tui/mission-model.js";
|
|
5
|
+
import { idleMotion, motionTiming, planMotion, sampleMotion } from "./tui/mission-motion.js";
|
|
6
|
+
import { chipBudget, chipLine, homeLine, missionPanel, paletteFromTheme, panelWidth } from "./tui/mission-text.js";
|
|
7
|
+
import { createLedgerReader, missionProjectRoot, readMissionInput } from "./tui/mission-source.js";
|
|
8
|
+
import { bandCard, bandRibbon, readBandOptions } from "./tui/mission-band.js";
|
|
9
|
+
export const litOpenCodeTuiPluginId = "litopencode";
|
|
10
|
+
const homeLineWidth = 69;
|
|
11
|
+
const refreshDelayMs = 80;
|
|
12
|
+
const ledgerPollMs = 2000;
|
|
13
|
+
const toastMs = 4000;
|
|
14
|
+
const trackedSessionLimit = 4;
|
|
15
|
+
const refreshEvents = [
|
|
16
|
+
"message.updated",
|
|
17
|
+
"message.part.updated",
|
|
18
|
+
"todo.updated",
|
|
19
|
+
"session.status",
|
|
20
|
+
"session.idle",
|
|
21
|
+
"session.error",
|
|
22
|
+
"permission.asked",
|
|
23
|
+
"permission.replied",
|
|
24
|
+
"question.asked",
|
|
25
|
+
"question.replied",
|
|
26
|
+
"question.rejected"
|
|
27
|
+
];
|
|
28
|
+
function spanFor(segment) {
|
|
29
|
+
return jsx("span", {
|
|
30
|
+
style: { fg: segment.fg, ...(segment.bg === undefined ? {} : { bg: segment.bg }), ...(segment.bold === true ? { bold: true } : {}) },
|
|
31
|
+
children: segment.text
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function lineElement(line) {
|
|
35
|
+
return jsx("text", { wrapMode: "none", children: line.length === 0 ? " " : line.map(spanFor) });
|
|
36
|
+
}
|
|
37
|
+
const tui = async (api, options) => {
|
|
38
|
+
const motionEnabled = options?.motion !== false;
|
|
39
|
+
const band = readBandOptions(options);
|
|
40
|
+
const readLedger = createLedgerReader();
|
|
41
|
+
const sessions = new Map();
|
|
42
|
+
const tracked = [];
|
|
43
|
+
const [revision, setRevision] = createSignal(0);
|
|
44
|
+
const [clock, setClock] = createSignal(Date.now());
|
|
45
|
+
let frameTimer;
|
|
46
|
+
let refreshTimer;
|
|
47
|
+
let pollTimer;
|
|
48
|
+
const state = () => api.state;
|
|
49
|
+
const palette = () => paletteFromTheme(api.theme.current, api.theme.mode());
|
|
50
|
+
// OpenCode keeps the sidebar choice in its KV store under "sidebar" ("auto" or "hide"); a child
|
|
51
|
+
// session never shows the sidebar. Before the store is ready the setting is unknown.
|
|
52
|
+
function sidebarSetting() {
|
|
53
|
+
if (!api.kv.ready)
|
|
54
|
+
return undefined;
|
|
55
|
+
const value = api.kv.get("sidebar", "auto");
|
|
56
|
+
return typeof value === "string" ? value : undefined;
|
|
57
|
+
}
|
|
58
|
+
function isChildSession(sessionID) {
|
|
59
|
+
const session = api.state.session.get(sessionID);
|
|
60
|
+
return typeof session?.parentID === "string" && session.parentID !== "";
|
|
61
|
+
}
|
|
62
|
+
function anyMoving(now) {
|
|
63
|
+
for (const entry of sessions.values())
|
|
64
|
+
if (sampleMotion(entry.motion, now).moving)
|
|
65
|
+
return true;
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
// The frame timer runs only while a track is moving; an idle session draws nothing new.
|
|
69
|
+
function ensureFrames() {
|
|
70
|
+
if (frameTimer !== undefined || !anyMoving(Date.now()))
|
|
71
|
+
return;
|
|
72
|
+
frameTimer = setInterval(() => {
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
setClock(now);
|
|
75
|
+
if (!anyMoving(now)) {
|
|
76
|
+
clearInterval(frameTimer);
|
|
77
|
+
frameTimer = undefined;
|
|
78
|
+
}
|
|
79
|
+
}, motionTiming.frameMs);
|
|
80
|
+
}
|
|
81
|
+
function refresh(sessionID) {
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
const source = state();
|
|
84
|
+
const ledger = readLedger(missionProjectRoot(source.path));
|
|
85
|
+
const next = buildMissionView(readMissionInput(source, sessionID, ledger, now));
|
|
86
|
+
const entry = sessions.get(sessionID);
|
|
87
|
+
if (entry !== undefined && JSON.stringify(entry.view) === JSON.stringify(next))
|
|
88
|
+
return;
|
|
89
|
+
if (entry !== undefined) {
|
|
90
|
+
const notice = leadingNotice(missionNotices(entry.view, next));
|
|
91
|
+
if (notice !== undefined)
|
|
92
|
+
api.ui.toast({ variant: notice.variant, title: notice.title, message: notice.message, duration: toastMs });
|
|
93
|
+
}
|
|
94
|
+
sessions.set(sessionID, { view: next, motion: planMotion(entry?.view, next, now, entry?.motion ?? idleMotion, motionEnabled) });
|
|
95
|
+
setClock(now);
|
|
96
|
+
setRevision(revision() + 1);
|
|
97
|
+
ensureFrames();
|
|
98
|
+
}
|
|
99
|
+
function refreshTracked() {
|
|
100
|
+
refreshTimer = undefined;
|
|
101
|
+
for (const sessionID of tracked)
|
|
102
|
+
refresh(sessionID);
|
|
103
|
+
syncPoll();
|
|
104
|
+
}
|
|
105
|
+
// Ledger writes from a terminal or another session fire no TUI event, so an active mission
|
|
106
|
+
// re-reads goals.json (one lstat while unchanged) every two seconds. Idle sessions do not poll.
|
|
107
|
+
function syncPoll() {
|
|
108
|
+
const active = [...sessions.values()].some((entry) => entry.view.active);
|
|
109
|
+
if (active && pollTimer === undefined)
|
|
110
|
+
pollTimer = setInterval(refreshTracked, ledgerPollMs);
|
|
111
|
+
if (!active && pollTimer !== undefined) {
|
|
112
|
+
clearInterval(pollTimer);
|
|
113
|
+
pollTimer = undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function scheduleRefresh() {
|
|
117
|
+
if (refreshTimer === undefined)
|
|
118
|
+
refreshTimer = setTimeout(refreshTracked, refreshDelayMs);
|
|
119
|
+
}
|
|
120
|
+
function track(sessionID) {
|
|
121
|
+
if (tracked.includes(sessionID))
|
|
122
|
+
return;
|
|
123
|
+
tracked.push(sessionID);
|
|
124
|
+
if (tracked.length > trackedSessionLimit)
|
|
125
|
+
sessions.delete(tracked.shift() ?? "");
|
|
126
|
+
queueMicrotask(() => {
|
|
127
|
+
refresh(sessionID);
|
|
128
|
+
syncPoll();
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
// Opening a session loads its messages and parts over HTTP, which fires no bus event. Reading the
|
|
132
|
+
// reactive store here re-runs this effect as they arrive, so a resumed session lights up too.
|
|
133
|
+
function watchSession(sessionID) {
|
|
134
|
+
createEffect(() => {
|
|
135
|
+
const source = state();
|
|
136
|
+
for (const message of source.session.messages(sessionID)) {
|
|
137
|
+
if (message.role === "user")
|
|
138
|
+
void source.part(message.id).length;
|
|
139
|
+
}
|
|
140
|
+
void source.session.todo(sessionID).length;
|
|
141
|
+
void source.session.status(sessionID)?.type;
|
|
142
|
+
scheduleRefresh();
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function sessionView(sessionID) {
|
|
146
|
+
return () => {
|
|
147
|
+
revision();
|
|
148
|
+
const entry = sessions.get(sessionID);
|
|
149
|
+
if (entry === undefined || !entry.view.active)
|
|
150
|
+
return undefined;
|
|
151
|
+
return { view: entry.view, frame: sampleMotion(entry.motion, clock()) };
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
for (const type of refreshEvents) {
|
|
155
|
+
api.event.on(type, scheduleRefresh);
|
|
156
|
+
}
|
|
157
|
+
api.lifecycle.onDispose(() => {
|
|
158
|
+
if (frameTimer !== undefined)
|
|
159
|
+
clearInterval(frameTimer);
|
|
160
|
+
if (refreshTimer !== undefined)
|
|
161
|
+
clearTimeout(refreshTimer);
|
|
162
|
+
if (pollTimer !== undefined)
|
|
163
|
+
clearInterval(pollTimer);
|
|
164
|
+
sessions.clear();
|
|
165
|
+
});
|
|
166
|
+
api.slots.register({
|
|
167
|
+
order: 50,
|
|
168
|
+
slots: {
|
|
169
|
+
session_prompt_right(_ctx, props) {
|
|
170
|
+
track(props.session_id);
|
|
171
|
+
watchSession(props.session_id);
|
|
172
|
+
const current = sessionView(props.session_id);
|
|
173
|
+
const dimensions = useTerminalDimensions();
|
|
174
|
+
const visible = createMemo(() => current() !== undefined);
|
|
175
|
+
return () => {
|
|
176
|
+
if (!visible())
|
|
177
|
+
return null;
|
|
178
|
+
return jsx("box", {
|
|
179
|
+
flexDirection: "row",
|
|
180
|
+
flexShrink: 0,
|
|
181
|
+
children: () => {
|
|
182
|
+
const snapshot = current();
|
|
183
|
+
if (snapshot === undefined)
|
|
184
|
+
return null;
|
|
185
|
+
const budget = chipBudget({ width: dimensions().width, sidebar: sidebarSetting(), child: isChildSession(props.session_id) });
|
|
186
|
+
return lineElement(chipLine(snapshot.view, snapshot.frame, palette(), budget));
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
},
|
|
191
|
+
sidebar_content(_ctx, props) {
|
|
192
|
+
track(props.session_id);
|
|
193
|
+
watchSession(props.session_id);
|
|
194
|
+
const current = sessionView(props.session_id);
|
|
195
|
+
const panel = createMemo(() => {
|
|
196
|
+
const snapshot = current();
|
|
197
|
+
return snapshot === undefined ? undefined : missionPanel(snapshot.view, snapshot.frame, palette());
|
|
198
|
+
});
|
|
199
|
+
const visible = createMemo(() => panel() !== undefined);
|
|
200
|
+
return () => {
|
|
201
|
+
if (!visible())
|
|
202
|
+
return null;
|
|
203
|
+
// Props are getters so the border, titles and fill follow each frame without rebuilding the box.
|
|
204
|
+
return jsx("box", {
|
|
205
|
+
border: true,
|
|
206
|
+
borderStyle: "rounded",
|
|
207
|
+
width: panelWidth,
|
|
208
|
+
flexShrink: 0,
|
|
209
|
+
flexDirection: "column",
|
|
210
|
+
paddingLeft: 1,
|
|
211
|
+
paddingRight: 1,
|
|
212
|
+
titleAlignment: "left",
|
|
213
|
+
bottomTitleAlignment: "right",
|
|
214
|
+
get borderColor() { return panel()?.border; },
|
|
215
|
+
get title() { return panel()?.title; },
|
|
216
|
+
get titleColor() { return panel()?.titleColor; },
|
|
217
|
+
get bottomTitle() { return panel()?.bottomTitle ?? ""; },
|
|
218
|
+
get backgroundColor() { return panel()?.background ?? "transparent"; },
|
|
219
|
+
children: () => (panel()?.rows ?? []).map(lineElement)
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
// The band: one full-width ribbon row under every route in app_bottom. It follows the session on
|
|
226
|
+
// screen; a child (subagent) session shows its parent's mission as a lane. Home shows no band.
|
|
227
|
+
if (band.enabled) {
|
|
228
|
+
api.slots.register({
|
|
229
|
+
order: 50,
|
|
230
|
+
slots: {
|
|
231
|
+
app_bottom() {
|
|
232
|
+
const dimensions = useTerminalDimensions();
|
|
233
|
+
const target = createMemo(() => {
|
|
234
|
+
const route = api.route.current;
|
|
235
|
+
const id = route.name === "session" && typeof route.params?.sessionID === "string" ? route.params.sessionID : undefined;
|
|
236
|
+
if (id === undefined)
|
|
237
|
+
return undefined;
|
|
238
|
+
const parent = api.state.session.get(id)?.parentID;
|
|
239
|
+
return typeof parent === "string" && parent !== "" ? { sessionID: parent, inLane: true } : { sessionID: id, inLane: false };
|
|
240
|
+
});
|
|
241
|
+
createEffect(() => {
|
|
242
|
+
const place = target();
|
|
243
|
+
if (place === undefined)
|
|
244
|
+
return;
|
|
245
|
+
track(place.sessionID);
|
|
246
|
+
watchSession(place.sessionID);
|
|
247
|
+
});
|
|
248
|
+
const current = createMemo(() => {
|
|
249
|
+
const place = target();
|
|
250
|
+
const snapshot = place === undefined ? undefined : sessionView(place.sessionID)();
|
|
251
|
+
return snapshot === undefined || place === undefined ? undefined : { ...snapshot, inLane: place.inLane };
|
|
252
|
+
});
|
|
253
|
+
const visible = createMemo(() => current() !== undefined);
|
|
254
|
+
return () => {
|
|
255
|
+
if (!visible())
|
|
256
|
+
return null;
|
|
257
|
+
return jsx("box", {
|
|
258
|
+
flexDirection: "column",
|
|
259
|
+
flexShrink: 0,
|
|
260
|
+
get backgroundColor() { return palette().panel ?? "transparent"; },
|
|
261
|
+
children: () => {
|
|
262
|
+
const snapshot = current();
|
|
263
|
+
if (snapshot === undefined)
|
|
264
|
+
return null;
|
|
265
|
+
const colors = palette();
|
|
266
|
+
const width = dimensions().width;
|
|
267
|
+
const rows = [
|
|
268
|
+
jsx("box", { paddingLeft: 2, paddingRight: 2, flexShrink: 0, children: lineElement(bandRibbon(snapshot.view, snapshot.frame, colors, width - 4, { inLane: snapshot.inLane })) })
|
|
269
|
+
];
|
|
270
|
+
const card = band.card ? bandCard(snapshot.view, snapshot.frame, colors, width - 2) : undefined;
|
|
271
|
+
if (card !== undefined) {
|
|
272
|
+
rows.push(jsx("box", {
|
|
273
|
+
border: true,
|
|
274
|
+
borderStyle: "rounded",
|
|
275
|
+
borderColor: card.border,
|
|
276
|
+
title: card.title,
|
|
277
|
+
titleColor: colors.text,
|
|
278
|
+
marginLeft: 1,
|
|
279
|
+
marginRight: 1,
|
|
280
|
+
paddingLeft: 1,
|
|
281
|
+
paddingRight: 1,
|
|
282
|
+
flexShrink: 0,
|
|
283
|
+
flexDirection: "column",
|
|
284
|
+
children: card.rows.map(lineElement)
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
return rows;
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
// After the built-in home tips (order 100): one line about the project's last mission, if any.
|
|
296
|
+
api.slots.register({
|
|
297
|
+
order: 150,
|
|
298
|
+
slots: {
|
|
299
|
+
home_bottom() {
|
|
300
|
+
const dimensions = useTerminalDimensions();
|
|
301
|
+
const line = createMemo(() => {
|
|
302
|
+
revision();
|
|
303
|
+
return homeLine(lastMission(readLedger(missionProjectRoot(state().path))), palette(), Math.min(homeLineWidth, dimensions().width - 10));
|
|
304
|
+
});
|
|
305
|
+
return () => {
|
|
306
|
+
const segments = line();
|
|
307
|
+
return segments.length === 0 ? null : jsx("box", { flexDirection: "row", justifyContent: "center", flexShrink: 0, children: lineElement(segments) });
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
};
|
|
313
|
+
const plugin = {
|
|
314
|
+
id: litOpenCodeTuiPluginId,
|
|
315
|
+
tui
|
|
316
|
+
};
|
|
317
|
+
export default plugin;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.19"><title>release: 1.0.19</title><g shape-rendering="crispEdges"><rect width="49" height="20" fill="#080d14"/><rect x="49" width="97" height="20" fill="#ff6337"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="255" y="140" textLength="390" transform="scale(.1)">release</text><text x="965" y="140" textLength="870" transform="scale(.1)" fill="#080D14">1.0.19</text></g></svg>
|
package/docs/reference-Ko-KR.md
CHANGED
|
@@ -242,6 +242,77 @@ npm uninstall -g @litfamily/litopencode
|
|
|
242
242
|
프로젝트의 `.litopencode/`는 별도 ledger와 receipt입니다. 나중에 재개할 계획이 없다면
|
|
243
243
|
그 상태를 버려도 되는지 확인한 뒤 직접 정리합니다.
|
|
244
244
|
|
|
245
|
+
TUI 패널 항목은 `tui.json`이나 `tui.jsonc`에 있습니다. `litopencode tui remove [--root <dir>]`는
|
|
246
|
+
두 파일에서 LitOpenCode 항목만 지우고, 다른 플러그인과 주석은 그대로 둡니다. 바뀌는 파일은 먼저
|
|
247
|
+
`<file>.litopencode-backup`으로 복사합니다. `--dry-run`은 같은 결과를 보여 주기만 하고 아무것도 쓰지 않습니다.
|
|
248
|
+
|
|
249
|
+
## TUI 미션 패널
|
|
250
|
+
|
|
251
|
+
OpenCode 1.18은 `tui.json`이나 `tui.jsonc`의 `plugin` 배열에 적힌 TUI 플러그인을 불러옵니다.
|
|
252
|
+
이 패키지는 TUI 모듈을 서버 플러그인과 따로 `@litfamily/litopencode/tui`(`dist/tui.js`, 플러그인 id
|
|
253
|
+
`litopencode`)로 내보냅니다. OpenCode가 두 대상을 한 모듈에 섞는 것을 허용하지 않기 때문입니다.
|
|
254
|
+
|
|
255
|
+
화면에 그리는 것:
|
|
256
|
+
|
|
257
|
+
- 입력창 아래 agent/model 줄 오른쪽의 단계 칩: `🔥 LIT` 알약 뒤에 `PLAN` › `WORK n/m` › `REVIEW`
|
|
258
|
+
알약과 다섯 칸 `▰▱` 막대가 붙습니다. 자리가 좁으면 이름부터 덜어 내서, `🔥`와 지금·다음 단계만,
|
|
259
|
+
더 좁으면 `🔥`와 개수만 남깁니다. 자리는 OpenCode에 저장된 사이드바 설정과 세션 종류(하위 세션에는
|
|
260
|
+
사이드바가 없음)로 계산합니다. 설정을 아직 읽을 수 없으면 사이드바가 열려 있다고 보고 좁게 잡습니다.
|
|
261
|
+
- 세션 사이드바 맨 위의 미션 패널: 36칸짜리 둥근 상자이고, 위 테두리에 `🔥 LIT ▸ <단계>`, 아래
|
|
262
|
+
테두리에 `n/m criteria`가 붙습니다. 테두리 색이 상태를 보여 줍니다. 일하는 중에는 테마 accent,
|
|
263
|
+
경고가 있으면 warning, 끝나면 success 색입니다. 안에는 두 줄짜리 `LIT`과 그 옆의 작업 이름·상태,
|
|
264
|
+
굵은 목표 제목, 단계 줄, 1/8칸 블록으로 칠하고 빈 곳은 가는 선으로 둔 그라데이션 막대, 다음 할 일이나
|
|
265
|
+
아직 통과하지 못한 기준, 기준 목록, 경고, 최근 증거 네 개가 들어갑니다.
|
|
266
|
+
- 띠(band): 화면 맨 아래 전체 폭에 붙는 리본 한 줄입니다(OpenCode의 `app_bottom` 자리). 세션과
|
|
267
|
+
사이드바 아래를 모두 가로지릅니다. 알약, 1/8칸 막대, 다음 할 일, 첫 lit 턴부터 지난 시간(`⟳ 7m`)을
|
|
268
|
+
보여 줍니다. 경고가 열려 있는 동안에는 `▸ 다음 일` 자리에 `⚑ <내용>`이 들어갑니다. 세션의 첫 lit
|
|
269
|
+
턴에 나타나 lit 세션 내내 남으므로, 입력창은 한 번만 움직입니다. 하위(subagent) 세션에서는 부모
|
|
270
|
+
세션의 미션을 `· in lane`과 함께 보여 주고, 홈 화면에는 띠가 없습니다. 폭이 좁으면 단계 이름,
|
|
271
|
+
다음 할 일 글자, 막대 길이, 시간 순서로 덜어 내며 줄을 넘기지 않습니다. 완료 순간도 여기서 함께 보입니다.
|
|
272
|
+
- 홈 화면 입력창 아래, OpenCode 팁 다음 줄: `🔥 lit ready · last: <목표> ✓ n/m`. 프로젝트 ledger에
|
|
273
|
+
목표가 있을 때만 보입니다.
|
|
274
|
+
- 경고는 색만 바꾸지 않고 늘 `⚑`와 글자를 함께 붙입니다. accent와 warning이 같은 색인 테마가 있기
|
|
275
|
+
때문입니다. 알파가 0인 테마 바탕(system, lucent-orng)은 검게 칠하지 않고 비워 둡니다.
|
|
276
|
+
- 관문 사건마다 토스트 하나: 계획 저장 또는 저장 실패, 목표 설정, 기준 통과, 리뷰 막힘과 해결, 완료.
|
|
277
|
+
한 번에 여러 사건이 생기면 가장 급한 것 하나로 합칩니다.
|
|
278
|
+
- 목표가 끝나거나 할 일이 모두 끝나면 1.2초 안에 끝나는 완료 순간을 보여 줍니다. 테두리가 초록으로
|
|
279
|
+
바뀌고 막대가 끝까지 차며, `WORK`와 `REVIEW`가 차례로 완료로 바뀝니다. 브랜드에 빛이 한 번 지나가고,
|
|
280
|
+
패널 안으로 불씨가 올라가며, `COMPLETE`가 살짝 넘쳤다가 제자리로 옵니다.
|
|
281
|
+
|
|
282
|
+
데이터는 이미 있는 것만 읽고, TUI를 위해 새로 쓰는 파일은 없습니다.
|
|
283
|
+
|
|
284
|
+
- `lit`이 모드를 켜거나 명령이 실행될 때(`save-plan` 포함) 이 플러그인이 사용자 메시지 part에 붙이는
|
|
285
|
+
`metadata.litopencode` 표시. 표시가 없는 세션은 쉬는 상태로 보고, 기본 `lit-loop` agent여도 칩과
|
|
286
|
+
패널을 숨깁니다.
|
|
287
|
+
- 세션의 OpenCode 할 일 목록(진행도와 다음 항목), 상태(재시도), 대기 중인 권한 요청과 질문, 마지막
|
|
288
|
+
assistant 메시지 기준 context 사용량(75%부터 주황).
|
|
289
|
+
- 프로젝트 루트의 `.litopencode/litgoal/lit-loop/goals.json`. 심볼릭 링크와 1 MiB가 넘는 파일은 읽지
|
|
290
|
+
않습니다. 세션의 첫 lit 턴보다 먼저 마지막으로 바뀐 목표는 이전 작업으로 보고 빼 둡니다.
|
|
291
|
+
|
|
292
|
+
움직임은 전환이 일어나는 동안에만 짧은 프레임 타이머로 돌고 끝나면 멈춥니다. 새 줄과 알약은
|
|
293
|
+
ease-out-back으로 160ms씩, 60ms 간격으로 들어오고, 막대는 300ms에 차오릅니다. 경고 줄은 220ms에
|
|
294
|
+
걸쳐 들어오고 색은 140ms 동안 넘어가며, 빛이 지나가는 데는 400ms가 걸립니다. lit 세션이
|
|
295
|
+
열려 있는 동안만 2초마다 목표 ledger를 다시 읽고, 쉬는 세션은 읽지 않습니다. 색은 지금 OpenCode
|
|
296
|
+
테마에서 가져옵니다. 움직임을 끄려면 `tui.json`에 `["@litfamily/litopencode@<version>", { "motion": false }]`
|
|
297
|
+
처럼 적고, 플러그인 자체를 끄려면 OpenCode 플러그인 관리자나 `plugin_enabled`에서 id `litopencode`를 끕니다.
|
|
298
|
+
|
|
299
|
+
띠 설정도 같은 옵션 객체에 적습니다.
|
|
300
|
+
|
|
301
|
+
- `{ "band": false }`는 띠만 없애고 칩, 패널, 홈 줄은 그대로 둡니다.
|
|
302
|
+
- `{ "band": { "card": true } }`는 리본 아래에 경고, 다음 할 일, 목표를 담은 둥근 카드를 붙입니다.
|
|
303
|
+
목표가 있는 동안 카드는 네 줄로 고정되어, 경고가 생기거나 사라져도 입력창이 움직이지 않습니다.
|
|
304
|
+
카드는 기본으로 꺼져 있습니다.
|
|
305
|
+
|
|
306
|
+
다른 터미널에서 쓴 ledger(예: `litopencode record-evidence`)는 OpenCode 이벤트를 만들지 않습니다.
|
|
307
|
+
그래서 2초마다 도는 공용 ledger 확인이 이를 잡고, 칩·패널·띠가 같은 프레임 시계로 함께 바뀝니다.
|
|
308
|
+
|
|
309
|
+
항목은 install이 씁니다. 기본 root에서는 `opencode plugin`이 `./tui` export를 보고 직접 항목을 넣고,
|
|
310
|
+
LitOpenCode가 중복과 옛 버전을 정리합니다. custom root에서는 LitOpenCode가 직접 씁니다. 이미
|
|
311
|
+
LitOpenCode 항목이 있는 파일을 고치고, 없으면 `tui.json`과 `tui.jsonc` 중 먼저 있는 파일을, 둘 다 없으면
|
|
312
|
+
새 `tui.json`을 씁니다. OpenCode가 고르는 방식과 같습니다. `./herdr-tui-session.js` 같은 다른 항목과
|
|
313
|
+
모든 주석은 그대로 남습니다. `plugin` 목록 안의 주석을 지워야 하는 경우에는 아무것도 쓰기 전에 install을
|
|
314
|
+
멈춥니다. `doctor`는 이 항목을 `install.tui`에 보고하고, `install.ok` 값은 바꾸지 않습니다.
|
|
315
|
+
|
|
245
316
|
## 자동 인수인계
|
|
246
317
|
|
|
247
318
|
자동 인수인계는 직접 켜야 하며 기본은 꺼져 있습니다. 기본 퍼센트는 없습니다. 플러그인의 `event` 훅과 `experimental.chat.system.transform` 훅에서 동작하고, 대화 내용은 읽지 않으며 assistant 메시지의 토큰 수만 봅니다.
|
|
@@ -338,7 +409,7 @@ OpenCode는 세션이 바쁜 동안 들어온 `summarize` 호출을 줄 세웠
|
|
|
338
409
|
|
|
339
410
|
- 패키지와 plugin id: `litopencode`
|
|
340
411
|
- CLI binary: `litopencode`
|
|
341
|
-
- 공개 export: `litopencode`, `litopencode/server`
|
|
412
|
+
- 공개 export: `litopencode`, `litopencode/server`, `litopencode/tui`
|
|
342
413
|
- 전역 route 파일: `~/.config/opencode/litopencode.json`
|
|
343
414
|
- 프로젝트 override: `.litopencode/config.json`
|
|
344
415
|
- Durable 상태: `.litopencode/litgoal/`
|
package/docs/reference.md
CHANGED
|
@@ -342,6 +342,87 @@ OpenCode's managed `opencode.jsonc` (or the custom root's `opencode.json`). Remo
|
|
|
342
342
|
Project `.litopencode/` state is separate; preserve it for later resume or remove it only
|
|
343
343
|
when discarding that project's ledger and receipts is intentional.
|
|
344
344
|
|
|
345
|
+
The TUI panel entry lives in `tui.json` or `tui.jsonc`. `litopencode tui remove [--root <dir>]`
|
|
346
|
+
deletes LitOpenCode's own entry from both files, keeps every other plugin and every comment, and
|
|
347
|
+
copies each changed file to `<file>.litopencode-backup` first. `--dry-run` prints the same receipt
|
|
348
|
+
and writes nothing.
|
|
349
|
+
|
|
350
|
+
## TUI mission panel
|
|
351
|
+
|
|
352
|
+
OpenCode 1.18 loads TUI plugins listed in the `plugin` array of `tui.json` or `tui.jsonc`. The
|
|
353
|
+
package exports its TUI module as `@litfamily/litopencode/tui` (`dist/tui.js`, plugin id
|
|
354
|
+
`litopencode`), separate from the server plugin, as OpenCode requires.
|
|
355
|
+
|
|
356
|
+
What it draws:
|
|
357
|
+
|
|
358
|
+
- A phase chip on the right of the prompt's agent/model row: a `🔥 LIT` pill, then `PLAN` ›
|
|
359
|
+
`WORK n/m` › `REVIEW` pills and a five-segment `▰▱` bar. When the row is narrower it drops the
|
|
360
|
+
labels first: `🔥` with the current and next phase, then `🔥` with only the count. The room comes
|
|
361
|
+
from OpenCode's stored sidebar setting and the session kind (a child session has no sidebar); when
|
|
362
|
+
the setting cannot be read yet, the chip assumes the sidebar is open and stays narrower.
|
|
363
|
+
- A mission panel at the top of the session sidebar, in a 36-cell rounded box titled
|
|
364
|
+
`🔥 LIT ▸ <PHASE>` with `n/m criteria` in the bottom border. The border carries the state: the
|
|
365
|
+
theme accent while working, the warning colour on a warning, the success colour when complete.
|
|
366
|
+
Inside: a two-row `LIT` with the discipline and a status beside it, the goal title in bold, the
|
|
367
|
+
phase line, a gradient bar drawn in eighth-cell blocks over a thin rule, the next todo or open
|
|
368
|
+
criterion, the criteria, warnings, and the last four evidence events.
|
|
369
|
+
- A band: one full-width ribbon row at the bottom of the screen (OpenCode's `app_bottom` slot), under
|
|
370
|
+
the session and the sidebar alike. It shows the pills, an eighth-block bar, the next task and the
|
|
371
|
+
time since the first lit turn (`⟳ 7m`). While a warning is open, `⚑ <text>` takes the place of
|
|
372
|
+
`▸ next`. The row appears at the session's first lit turn and stays for the whole lit session, so
|
|
373
|
+
the prompt moves only once. A subagent session shows its parent's mission with `· in lane`; the
|
|
374
|
+
home screen has no band. When the row is narrow it drops, in order, the step labels, the next-task
|
|
375
|
+
text, bar width and then the time; it never wraps. The completion moment plays here too.
|
|
376
|
+
- A line under the home prompt, after OpenCode's tips: `🔥 lit ready · last: <goal> ✓ n/m`. It
|
|
377
|
+
appears only when the project ledger holds a goal.
|
|
378
|
+
- Warnings always carry `⚑` and words, not colour alone, because some themes use one colour for
|
|
379
|
+
both accent and warning. Theme surfaces with alpha 0 (the system and lucent-orng themes) are left
|
|
380
|
+
unfilled rather than painted black.
|
|
381
|
+
- One toast per refresh for a gate event: plan saved or not saved, goal set, criterion passed,
|
|
382
|
+
review blocker opened or resolved, and completion. Several events at once collapse into the most
|
|
383
|
+
urgent one.
|
|
384
|
+
- A completion moment of at most 1.2 seconds when the goal completes or every todo is done: the
|
|
385
|
+
border turns green, the bar fills, `WORK` then `REVIEW` turn done, a light sweep crosses the brand,
|
|
386
|
+
embers rise through the panel and `COMPLETE` pops in with a small overshoot before it settles.
|
|
387
|
+
|
|
388
|
+
Where the data comes from, with nothing new written for the TUI:
|
|
389
|
+
|
|
390
|
+
- The `metadata.litopencode` marks this plugin already attaches to user-message parts when `lit`
|
|
391
|
+
activates a mode or a command runs (`save-plan` included). A session with no mark is idle and the
|
|
392
|
+
chip and panel stay hidden, even on the default `lit-loop` agent.
|
|
393
|
+
- OpenCode's todo list for the session (progress and next item), its status (retry), pending
|
|
394
|
+
permissions and questions, and context use from the last assistant message (amber at 75%).
|
|
395
|
+
- `.litopencode/litgoal/lit-loop/goals.json` under the project root. Symbolic links and files over
|
|
396
|
+
1 MiB are ignored. A goal last updated before the session's first lit turn is treated as earlier
|
|
397
|
+
work and left out.
|
|
398
|
+
|
|
399
|
+
Motion runs on a short frame timer only while a transition plays and stops afterwards: new rows
|
|
400
|
+
and pills slide in with ease-out-back, 160 ms each and 60 ms apart; the bar fills in 300 ms; a
|
|
401
|
+
warning row slides in over 220 ms and colours cross-fade in 140 ms; the light sweep takes 400 ms. While a
|
|
402
|
+
lit session is open the panel re-reads the goal ledger every two seconds; an idle session does not.
|
|
403
|
+
Colours come from the active OpenCode theme. Set `["@litfamily/litopencode@<version>", { "motion": false }]`
|
|
404
|
+
in `tui.json` to keep it static, or turn the plugin off by id `litopencode` in OpenCode's plugin
|
|
405
|
+
manager or with `plugin_enabled`.
|
|
406
|
+
|
|
407
|
+
Band options go in the same options object:
|
|
408
|
+
|
|
409
|
+
- `{ "band": false }` removes the band and leaves the chip, the panel and the home line.
|
|
410
|
+
- `{ "band": { "card": true } }` adds a rounded card under the ribbon with the warnings, the next
|
|
411
|
+
task and the goal. While a goal exists the card keeps a fixed four rows, so a warning arriving or
|
|
412
|
+
clearing never moves the prompt. The card is off by default.
|
|
413
|
+
|
|
414
|
+
A ledger written from another terminal (for example `litopencode record-evidence`) raises no
|
|
415
|
+
OpenCode event, so the shared two-second ledger check picks it up; the chip, the panel and the band
|
|
416
|
+
then change on the same frame clock.
|
|
417
|
+
|
|
418
|
+
Install writes the entry. On the default root `opencode plugin` adds it from the `./tui` export and
|
|
419
|
+
LitOpenCode then removes duplicates and stale versions; on a custom root LitOpenCode writes it. The
|
|
420
|
+
file that already holds a LitOpenCode entry is edited, otherwise the first of `tui.json` and
|
|
421
|
+
`tui.jsonc` that exists, otherwise a new `tui.json`, matching OpenCode's own choice. Other entries
|
|
422
|
+
such as `./herdr-tui-session.js` and all comments stay; an edit that would delete a comment inside
|
|
423
|
+
the `plugin` list stops the install before anything is written. `doctor` reports the entry under
|
|
424
|
+
`install.tui` without changing `install.ok`.
|
|
425
|
+
|
|
345
426
|
## Automatic handoff
|
|
346
427
|
|
|
347
428
|
Automatic handoff is opt-in and OFF by default. There is no default percent. It works in the plugin's `event` hook and its `experimental.chat.system.transform` hook, and it reads nothing from the conversation except each assistant message's token counts.
|
|
@@ -453,7 +534,7 @@ only. It is sent only in the `Authorization` header and is never written, logged
|
|
|
453
534
|
|
|
454
535
|
- Package and plugin id: `litopencode`
|
|
455
536
|
- CLI binary: `litopencode`
|
|
456
|
-
- Public exports: `litopencode` and `litopencode/
|
|
537
|
+
- Public exports: `litopencode`, `litopencode/server` and `litopencode/tui`
|
|
457
538
|
- Global route file: `~/.config/opencode/litopencode.json`
|
|
458
539
|
- Project overrides: `.litopencode/config.json`
|
|
459
540
|
- Durable state: `.litopencode/litgoal/`
|
package/package.json
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@litfamily/litopencode",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.19",
|
|
4
|
+
"description": "Workflow plugin for OpenCode: add lit to a prompt and the work is planned, built, checked and written down so the next session can pick it up.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"opencode",
|
|
7
|
+
"opencode-plugin",
|
|
8
|
+
"ai-agents",
|
|
9
|
+
"coding-agent",
|
|
10
|
+
"agent-workflow",
|
|
11
|
+
"planning",
|
|
12
|
+
"code-review",
|
|
13
|
+
"research",
|
|
14
|
+
"context-handoff",
|
|
15
|
+
"developer-tools",
|
|
16
|
+
"korean",
|
|
17
|
+
"docx",
|
|
18
|
+
"pptx"
|
|
19
|
+
],
|
|
5
20
|
"license": "MIT",
|
|
6
21
|
"repository": {
|
|
7
22
|
"type": "git",
|
|
@@ -22,6 +37,10 @@
|
|
|
22
37
|
"./server": {
|
|
23
38
|
"types": "./dist/server.d.ts",
|
|
24
39
|
"import": "./dist/server.js"
|
|
40
|
+
},
|
|
41
|
+
"./tui": {
|
|
42
|
+
"types": "./dist/tui.d.ts",
|
|
43
|
+
"import": "./dist/tui.js"
|
|
25
44
|
}
|
|
26
45
|
},
|
|
27
46
|
"scripts": {
|
|
@@ -58,7 +58,7 @@ LABELS = {
|
|
|
58
58
|
}
|
|
59
59
|
PACK_KEYS = {"schema_version", "tonality", "summary", "dials", "docx", "design"}
|
|
60
60
|
DESIGN_KEYS = {"ramp", "numbering", "h1_rule", "title_block", "contents", "summary_form", "conclusion_first", "running_head", "justify",
|
|
61
|
-
"palette", "accent_on", "page_geometry", "components", "figure_style", "
|
|
61
|
+
"palette", "accent_on", "page_geometry", "components", "figure_style", "spacing"}
|
|
62
62
|
ACCENT_PLACES = ("title_rule", "callout_rule", "sidebar_rule")
|
|
63
63
|
DIRECTIVE_KEYS = {
|
|
64
64
|
"cover": {"variant", "image", "kicker"},
|
|
@@ -34,7 +34,7 @@ colour; the pack's own accent is a quiet navy. Pack file: `templates/tonalities/
|
|
|
34
34
|
## Type
|
|
35
35
|
|
|
36
36
|
Pretendard throughout. Compact ramp at density 10. Titles at normal tracking, display slightly
|
|
37
|
-
tight
|
|
37
|
+
tight.
|
|
38
38
|
|
|
39
39
|
## Dials
|
|
40
40
|
|
|
@@ -73,9 +73,8 @@ Display devices: cover `plate`, statement `drench`, big number on a tint, closin
|
|
|
73
73
|
|
|
74
74
|
- Tables `light-grid`: no header fill, row rules, no banding, numbers right-aligned, totals bold.
|
|
75
75
|
A table can only appear inside a family that holds one (kpi-row, a closing).
|
|
76
|
-
- Charts: hairline gridlines, direct labels
|
|
77
|
-
- Images:
|
|
78
|
-
proportions, never stretched.
|
|
76
|
+
- Charts: hairline gridlines, direct labels.
|
|
77
|
+
- Images: no frame; pictures are cropped to their box with their own proportions, never stretched.
|
|
79
78
|
|
|
80
79
|
## Decoration vocabulary
|
|
81
80
|
|
|
@@ -74,8 +74,8 @@ Display devices: cover `rail`, statement `drench`, big number on a tint, closing
|
|
|
74
74
|
|
|
75
75
|
- Tables `header-fill`: a pale apricot header with dark rust labels, row rules, no banding, numbers
|
|
76
76
|
right-aligned, totals bold.
|
|
77
|
-
- Charts (if a chart appears in a table-insight): hairline gridlines, direct labels
|
|
78
|
-
- Images
|
|
77
|
+
- Charts (if a chart appears in a table-insight): hairline gridlines, direct labels.
|
|
78
|
+
- Images: no frame.
|
|
79
79
|
|
|
80
80
|
## Decoration vocabulary
|
|
81
81
|
|
|
@@ -33,7 +33,7 @@ Pack file: `templates/tonalities/gazette/pack.yaml`.
|
|
|
33
33
|
|
|
34
34
|
## Type
|
|
35
35
|
|
|
36
|
-
Pretendard throughout. Compact ramp at density 10 (body 13 pt, tables 11 pt).
|
|
36
|
+
Pretendard throughout. Compact ramp at density 10 (body 13 pt, tables 11 pt).
|
|
37
37
|
|
|
38
38
|
## Dials
|
|
39
39
|
|
|
@@ -71,8 +71,8 @@ outline, and `index: true`: a section page repeats the agenda with the current p
|
|
|
71
71
|
|
|
72
72
|
- Tables `header-fill`: slate blue header with white labels, a full grid of rules, pale slate
|
|
73
73
|
banding, numbers right-aligned, totals bold. The densest table style of the eight.
|
|
74
|
-
- Charts: hairline gridlines, direct labels
|
|
75
|
-
- Images:
|
|
74
|
+
- Charts: hairline gridlines, direct labels.
|
|
75
|
+
- Images: hairline frame.
|
|
76
76
|
|
|
77
77
|
## Decoration vocabulary
|
|
78
78
|
|