@litfamily/litopencode 1.0.36 โ†’ 1.0.38

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README-Ko-KR.md +6 -6
  3. package/README.md +6 -6
  4. package/dist/state.d.ts +1 -0
  5. package/dist/state.js +1 -0
  6. package/dist/tools.d.ts +15 -5
  7. package/dist/tui/mission-band.d.ts +7 -0
  8. package/dist/tui/mission-band.js +4 -0
  9. package/dist/tui/mission-skills.js +3 -1
  10. package/dist/tui/mission-source-v2.d.ts +23 -0
  11. package/dist/tui/mission-source-v2.js +29 -4
  12. package/dist/tui/mission-source.d.ts +1 -0
  13. package/dist/tui/mission-source.js +3 -2
  14. package/dist/tui/mission-text.d.ts +3 -3
  15. package/dist/tui/mission-text.js +5 -5
  16. package/dist/v2/events.d.ts +1 -1
  17. package/dist/v2/events.js +31 -5
  18. package/dist/v2/notices.d.ts +13 -0
  19. package/dist/v2/notices.js +143 -0
  20. package/dist/v2/tui-footer.d.ts +20 -0
  21. package/dist/v2/tui-footer.js +56 -0
  22. package/dist/v2/tui-notices.d.ts +7 -0
  23. package/dist/v2/tui-notices.js +45 -0
  24. package/dist/v2/tui.d.ts +43 -1
  25. package/dist/v2/tui.js +345 -32
  26. package/dist/v2/usage-limits.d.ts +19 -0
  27. package/dist/v2/usage-limits.js +70 -0
  28. package/docs/assets/readme/badge-version.svg +1 -1
  29. package/docs/reference-Ko-KR.md +24 -5
  30. package/docs/reference.md +29 -7
  31. package/package.json +6 -2
  32. package/skills/lit-typographic-motion/engine/cover/render.mjs +1 -1
  33. package/skills/lit-typographic-motion/engine/stage/capture.mjs +3 -2
  34. package/skills/lit-typographic-motion/engine/stage/gate.mjs +1 -0
  35. package/skills/lit-typographic-motion/engine/stage/render.mjs +1 -0
  36. package/skills/lit-typographic-motion/engine/stage/scan.mjs +109 -5
  37. package/skills/lit-typographic-motion/engine/stage/stage-kit.js +39 -1
  38. package/skills/lit-typographic-motion/references/cover.md +2 -1
  39. package/skills/lit-typographic-motion/references/runtime.md +1 -1
  40. package/skills/lit-typographic-motion/references/stage.md +48 -2
  41. package/skills/lit-typographic-motion/requirements-audio.txt +92 -89
  42. package/skills/managed-skill-manifest.json +13 -13
  43. package/skills/readme-studio/templates/hyperframes/package-lock.json +4 -4
  44. package/skills/readme-studio/templates/remotion/package-lock.json +3 -3
  45. package/skills/readme-studio/templates/remotion/package.json +1 -1
package/dist/v2/tui.js CHANGED
@@ -1,26 +1,57 @@
1
- // LitOpenCode's OpenCode 2 TUI plugin: the mission panel in the session sidebar, the ignition line under
2
- // the prompt (`๐Ÿ”ฅ LIT IGNITED ยท <discipline>`, the OpenCode 2 home of the OpenCode 1 completion mark),
3
- // and toasts. OpenCode 2 loads it on its own for an active server plugin. The server cannot raise toasts
4
- // on OpenCode 2, so the ignition toast is inferred here from the lit mark the server writes into the user
5
- // message metadata. It only reads state. OpenTUI and Solid come from the host at run time; tests pass a
6
- // stand-in runtime.
1
+ // LitOpenCode's OpenCode 2 TUI plugin: the same HUD OpenCode 1 draws, placed in the slots OpenCode 2
2
+ // publishes (@opencode/plugin SlotMap). The status card sits after the prompt footer, the stage chip and
3
+ // the ignition line (`๐Ÿ”ฅ LIT IGNITED ยท <discipline>`, the OpenCode 2 home of the OpenCode 1 completion
4
+ // mark) share prompt.footer.status, the home line is in home.footer and the mission panel in
5
+ // sidebar.content. Toasts cover the gate events, the stage moving, discipline switches, lanes, the
6
+ // ignition and the restart notice, with an opt-in finish alert. OpenCode 2 loads the plugin on its own
7
+ // for an active server plugin. The server cannot raise toasts there, so the ignition toast is inferred
8
+ // from the lit mark the server writes into the user message metadata. It only reads state. OpenTUI and
9
+ // Solid come from the host at run time; tests pass a stand-in runtime.
7
10
  import { activationDiscipline } from "../activation-probe.js";
11
+ import { readPackageMetadata } from "../cli/json.js";
8
12
  import { micro, supportsMarkGlyphs } from "../lit-mark.js";
9
- import { buildMissionView, leadingNotice, missionNotices } from "../tui/mission-model.js";
13
+ import { readInstalledVersions, restartVersion } from "../restart-notice.js";
14
+ import { buildMissionView, lastMission, leadingNotice, missionNotices } from "../tui/mission-model.js";
10
15
  import { idleMotion, motionTiming, planMotion, sampleMotion } from "../tui/mission-motion.js";
11
- import { latestLitMarkV2, missionStateFromV2 } from "../tui/mission-source-v2.js";
12
- import { createLedgerReader, createWorkReader, missionProjectRoot, readLatestResearchRun, readMissionInput } from "../tui/mission-source.js";
13
- import { missionPanel, paletteFromTheme, panelWidth } from "../tui/mission-text.js";
16
+ import { latestLitMarkV2, missionStateFromV2, sessionMessagesV2 } from "../tui/mission-source-v2.js";
17
+ import { adhdSuggestDue, createLedgerReader, createWorkReader, earlierWorkWindowMs, hasEarlierWork, messageTimes, missionProjectRoot, readCardInput, readLatestResearchRun, readMissionInput, readThreadSources, wayBackDue } from "../tui/mission-source.js";
18
+ import { chipShimmer, idleChipMotion, planChipMotion } from "../tui/mission-skills.js";
19
+ import { bandCard, readBandOptions, readNotifyOptions, statusCard } from "../tui/mission-band.js";
20
+ import { activeSkillLook, turnGlow, waitingOnYou } from "../tui/mission-looks.js";
21
+ import { rainbowActive, rainbowAllowed, rainbowAt, rainbowShows, rainbowTickMs } from "../tui/mission-rainbow.js";
22
+ import { threadLineCount, threadsOf } from "../tui/mission-threads.js";
23
+ import { homeLine, missionPanel, paletteFromTheme, panelWidth } from "../tui/mission-text.js";
24
+ import { createRuntimePaths } from "../state.js";
25
+ import { footerBudget, footerStatus, sidebarCells } from "./tui-footer.js";
26
+ import { createNoticeTail } from "./notices.js";
27
+ import { v2StageNotices } from "./tui-notices.js";
28
+ import { createUsageLimits } from "./usage-limits.js";
14
29
  const refreshDelayMs = 80;
15
30
  const ledgerPollMs = 2000;
16
31
  const toastMs = 4000;
17
32
  const ignitionToastMs = 6000;
33
+ const liveTickMs = 1000;
34
+ const restartCheckMs = 10 * 60_000;
18
35
  const trackedSessionLimit = 4;
36
+ const homeLineWidth = 69;
19
37
  // A lit mark older than the moment a session is first drawn is history being loaded, not a new turn.
20
38
  const historySlackMs = 2000;
39
+ const defaultWidth = 120;
40
+ const sidebarMinWidth = 120;
41
+ // The plan windows change slowly; asking the host more than once a minute would only add requests.
42
+ const limitsPollMs = 60_000;
43
+ // Only the OpenCode Go allowance has a source on OpenCode 2, so the windows belong to a Go session alone.
44
+ const goProviderID = "opencode-go";
45
+ const noticeBurst = 3;
21
46
  async function hostRuntime() {
22
- const [jsxRuntime, solid] = await Promise.all([import("@opentui/solid/jsx-runtime"), import("solid-js")]);
23
- return { jsx: jsxRuntime.jsx, createSignal: solid.createSignal, createEffect: solid.createEffect, createRoot: solid.createRoot };
47
+ const [jsxRuntime, solid, opentui] = await Promise.all([import("@opentui/solid/jsx-runtime"), import("solid-js"), import("@opentui/solid")]);
48
+ return {
49
+ jsx: jsxRuntime.jsx,
50
+ createSignal: solid.createSignal,
51
+ createEffect: solid.createEffect,
52
+ createRoot: solid.createRoot,
53
+ useTerminalDimensions: opentui.useTerminalDimensions
54
+ };
24
55
  }
25
56
  /** OpenCode 2 theme tokens under the names the mission palette reads. */
26
57
  export function paletteFromV2Theme(theme, mode) {
@@ -42,23 +73,54 @@ export async function setupLitOpenCodeTui(context, runtime) {
42
73
  const ui = runtime ?? await hostRuntime();
43
74
  const [settings] = context.storage.store("settings", { initial: { motion: true } });
44
75
  const motionEnabled = settings.motion !== false;
76
+ const band = readBandOptions(context.options);
77
+ const notify = readNotifyOptions(context.options);
45
78
  const directory = context.location?.directory ?? context.data.location.default().directory;
46
79
  const root = missionProjectRoot({ directory });
47
80
  const readLedger = createLedgerReader();
48
81
  const readWork = createWorkReader();
82
+ const colorAllowed = rainbowAllowed(motionEnabled, process.env.NO_COLOR);
49
83
  const sessions = new Map();
50
84
  const ignitions = new Map();
51
85
  const trackedAt = new Map();
52
86
  const tracked = [];
87
+ const suggested = new Set();
88
+ const suggestedDays = new Map();
53
89
  // Each session's store watcher lives in its own root, so it outlasts the slot render that started it
54
90
  // and ends only on eviction or cleanup. After cleanup nothing may arm a timer again.
55
91
  const watchers = new Map();
56
92
  let disposed = false;
57
93
  const [revision, setRevision] = ui.createSignal(0);
58
94
  const [clock, setClock] = ui.createSignal(Date.now());
95
+ // The version this start loaded; an install newer than it shows `restart to <v>` and one toast.
96
+ const [restartTo, setRestartTo] = ui.createSignal(undefined);
59
97
  let frameTimer;
60
98
  let refreshTimer;
61
99
  let pollTimer;
100
+ let liveTimer;
101
+ let bandTimer;
102
+ let restartTimer;
103
+ const readUsage = context.client?.usage?.limits;
104
+ const limits = createUsageLimits({
105
+ read: readUsage === undefined ? undefined : () => readUsage.call(context.client?.usage),
106
+ now: Date.now,
107
+ intervalMs: limitsPollMs,
108
+ onChange: () => setRevision(revision() + 1)
109
+ });
110
+ const noticeTail = createNoticeTail(root === undefined ? "" : createRuntimePaths(root).noticesFile);
111
+ // The provider of the session's latest assistant message decides whether the Go allowance applies to it.
112
+ function goSession(sessionID) {
113
+ const latest = sessionMessagesV2(context.data, sessionID).filter((message) => message.type === "assistant").at(-1);
114
+ return latest?.model?.providerID === goProviderID;
115
+ }
116
+ // Popups the server could not raise (the automatic handoff) arrive as lines in the notices file.
117
+ function showServerNotices() {
118
+ if (disposed)
119
+ return;
120
+ for (const notice of noticeTail.poll().slice(-noticeBurst)) {
121
+ context.ui.toast.show({ title: notice.title, message: notice.message, variant: notice.variant, duration: notice.duration });
122
+ }
123
+ }
62
124
  const palette = () => paletteFromV2Theme(context.theme, context.themeMode);
63
125
  function span(segment) {
64
126
  return ui.jsx("span", {
@@ -69,17 +131,43 @@ export async function setupLitOpenCodeTui(context, runtime) {
69
131
  function line(segments) {
70
132
  return ui.jsx("text", { wrapMode: "none", children: segments.length === 0 ? " " : segments.map(span) });
71
133
  }
72
- function viewFor(sessionID, now) {
73
- const source = missionStateFromV2(context.data, directory);
74
- const records = { work: readWork(root, sessionID), research: readLatestResearchRun(root) };
75
- return buildMissionView(readMissionInput(source, sessionID, readLedger(root), now, {
76
- ...(records.work === undefined ? {} : { work: records.work }),
77
- ...(records.research === undefined ? {} : { research: records.research })
78
- }));
134
+ function state() {
135
+ return missionStateFromV2(context.data, directory);
136
+ }
137
+ function parentOf(sessionID) {
138
+ const parent = context.data.session.get?.(sessionID)?.parentID;
139
+ return typeof parent === "string" && parent !== "" ? parent : undefined;
140
+ }
141
+ function width() {
142
+ return ui.useTerminalDimensions?.()().width ?? defaultWidth;
143
+ }
144
+ function rainbowOn(entry, now) {
145
+ return colorAllowed && rainbowShows(rainbowActive(entry.view, entry.card.chips, entry.card.busy), entry.rainbowSeen, entry.lastPromptAt, now);
146
+ }
147
+ function bandStepping(entry) {
148
+ if (!colorAllowed)
149
+ return false;
150
+ if (entry.waiting || rainbowOn(entry, Date.now()))
151
+ return true;
152
+ return entry.card.threads?.some((row) => row.status === "working" && row.done === undefined) === true;
153
+ }
154
+ function syncBandTimer() {
155
+ if (disposed)
156
+ return;
157
+ const needed = [...sessions.values()].some(bandStepping);
158
+ if (needed && bandTimer === undefined)
159
+ bandTimer = setInterval(() => {
160
+ setClock(Date.now());
161
+ syncBandTimer();
162
+ }, rainbowTickMs);
163
+ if (!needed && bandTimer !== undefined) {
164
+ clearInterval(bandTimer);
165
+ bandTimer = undefined;
166
+ }
79
167
  }
80
168
  function anyMoving(now) {
81
169
  for (const entry of sessions.values())
82
- if (sampleMotion(entry.motion, now).moving)
170
+ if (sampleMotion(entry.motion, now).moving || chipShimmer(entry.chipMotion, now) !== undefined)
83
171
  return true;
84
172
  return false;
85
173
  }
@@ -95,11 +183,103 @@ export async function setupLitOpenCodeTui(context, runtime) {
95
183
  }
96
184
  }, motionTiming.frameMs);
97
185
  }
186
+ // The live step's elapsed time is the one steady change: a tick a second while a tool runs.
187
+ function syncLiveTick() {
188
+ if (disposed)
189
+ return;
190
+ const running = [...sessions.values()].some((entry) => entry.view.live !== undefined);
191
+ if (running && liveTimer === undefined)
192
+ liveTimer = setInterval(() => setClock(Date.now()), liveTickMs);
193
+ if (!running && liveTimer !== undefined) {
194
+ clearInterval(liveTimer);
195
+ liveTimer = undefined;
196
+ }
197
+ }
198
+ // The card keeps the tallest lane count it reached while lanes are live or a turn runs, so the prompt does not jump.
199
+ function holdRows(held, rows, busy) {
200
+ if (rows > 0)
201
+ return Math.max(held, rows);
202
+ return busy ? held : 0;
203
+ }
98
204
  function ignitionToast(sessionID, name) {
99
205
  const label = `${ignitionLine(name)} ๐Ÿ”ฅ`;
100
206
  const message = supportsMarkGlyphs() ? [...micro.map((row) => row.replace(/ +$/u, "")), label].join("\n") : label;
101
207
  context.ui.toast.show({ title: "๐Ÿ”ฅ LIT IGNITED", message, variant: "warning", duration: ignitionToastMs, sessionID });
102
208
  }
209
+ // The finish alert is off unless the plugin options opt in; OpenCode's own attention settings still
210
+ // decide whether a desktop notification or a sound actually plays.
211
+ function alertFinish(notice) {
212
+ if (!notify.enabled || context.attention === undefined)
213
+ return;
214
+ void context.attention
215
+ .notify({ title: "๐Ÿ”ฅ LIT complete", message: notice.message, notification: notify.desktop, sound: notify.sound ? { name: "done" } : false })
216
+ .catch(() => undefined);
217
+ }
218
+ async function checkRestart() {
219
+ const loaded = await readPackageMetadata().catch(() => undefined);
220
+ if (loaded === undefined || disposed)
221
+ return;
222
+ const target = restartVersion(loaded.version, await readInstalledVersions(loaded.packageRoot).catch(() => []));
223
+ if (disposed || target === undefined || target === restartTo())
224
+ return;
225
+ if (restartTo() === undefined) {
226
+ context.ui.toast.show({ variant: "info", title: "๐Ÿ”ฅ LIT", message: `LitOpenCode ${target} is installed. Restart OpenCode to load it.`, duration: toastMs });
227
+ }
228
+ setRestartTo(target);
229
+ }
230
+ // One toast per session, for a fresh root session in a workspace that has earlier work to pick up.
231
+ function suggestAdhd(sessionID, source) {
232
+ if (suggested.has(sessionID) || parentOf(sessionID) !== undefined)
233
+ return;
234
+ suggested.add(sessionID);
235
+ const created = context.data.session.get?.(sessionID)?.time?.created;
236
+ const now = Date.now();
237
+ if (source.session.messages(sessionID).length > 0 || typeof created !== "number" || now - created > 2 * 60_000)
238
+ return;
239
+ const projectRoot = missionProjectRoot(source.path);
240
+ if (projectRoot === undefined || !hasEarlierWork(projectRoot, sessionID, now, earlierWorkWindowMs) || !adhdSuggestDue(suggestedDays, projectRoot, now))
241
+ return;
242
+ context.ui.toast.show({ variant: "info", title: "๐Ÿ”ฅ LIT", message: "Earlier work here. Say lit adhd to pick up where you left off.", duration: toastMs });
243
+ }
244
+ // One session's view, card and the notices from its previous entry. Pure reads; nothing is armed here.
245
+ function build(sessionID, entry, now) {
246
+ const source = state();
247
+ const work = readWork(root, sessionID);
248
+ const research = readLatestResearchRun(root);
249
+ const view = buildMissionView(readMissionInput(source, sessionID, readLedger(root), now, {
250
+ ...(work === undefined ? {} : { work }),
251
+ ...(research === undefined ? {} : { research })
252
+ }));
253
+ const threads = threadsOf(readThreadSources(source, sessionID, root, now), now);
254
+ const times = messageTimes(source, sessionID);
255
+ const card = {
256
+ ...readCardInput(source, sessionID),
257
+ ...(threads === undefined ? {} : { threads }),
258
+ ...(wayBackDue(times.lastAt, now, hasEarlierWork(root, sessionID, now, earlierWorkWindowMs)) ? { earlier: true } : {})
259
+ };
260
+ const viewChanged = entry === undefined || JSON.stringify(entry.view) !== JSON.stringify(view);
261
+ const rainbowSeen = rainbowActive(view, card.chips, card.busy) ? now : entry?.rainbowSeen;
262
+ const waiting = waitingOnYou(view, card.busy, { asked: card.asked === true, userSpokeLast: card.userSpokeLast === true, blockedReply: card.blockedReply === true, boundary: work?.boundary === true });
263
+ const changed = entry === undefined || viewChanged || entry.lastPromptAt !== times.lastPromptAt || JSON.stringify(entry.card) !== JSON.stringify(card) || entry.rainbowSeen !== rainbowSeen || entry.waiting !== waiting;
264
+ const lanesChanged = entry !== undefined && JSON.stringify(entry.card.threads) !== JSON.stringify(card.threads);
265
+ const notices = entry !== undefined && (viewChanged || lanesChanged)
266
+ ? [...missionNotices(entry.view, view), ...v2StageNotices(entry.view, view, entry.card.threads ?? [], card.threads ?? [])]
267
+ : [];
268
+ return {
269
+ changed,
270
+ notices,
271
+ next: {
272
+ view,
273
+ card,
274
+ motion: entry !== undefined && viewChanged ? planMotion(entry.view, view, now, entry.motion, motionEnabled) : (entry?.motion ?? idleMotion),
275
+ chipMotion: planChipMotion(entry?.card.chips, card.chips, now, entry?.chipMotion ?? idleChipMotion, motionEnabled),
276
+ reserve: holdRows(entry?.reserve ?? 0, threadLineCount(threads), card.busy),
277
+ ...(rainbowSeen === undefined ? {} : { rainbowSeen }),
278
+ ...(times.lastPromptAt === undefined ? {} : { lastPromptAt: times.lastPromptAt }),
279
+ waiting
280
+ }
281
+ };
282
+ }
103
283
  function refresh(sessionID) {
104
284
  if (disposed)
105
285
  return;
@@ -110,19 +290,25 @@ export async function setupLitOpenCodeTui(context, runtime) {
110
290
  if (mark !== undefined && mark.at >= (trackedAt.get(sessionID) ?? 0) - historySlackMs)
111
291
  ignitionToast(sessionID, mark.name);
112
292
  }
113
- const next = viewFor(sessionID, now);
293
+ if (goSession(sessionID))
294
+ void limits.poll();
114
295
  const entry = sessions.get(sessionID);
115
- if (entry !== undefined && JSON.stringify(entry.view) === JSON.stringify(next))
296
+ const { next, changed, notices } = build(sessionID, entry, now);
297
+ if (!changed)
116
298
  return;
117
- if (entry !== undefined) {
118
- const notice = leadingNotice(missionNotices(entry.view, next));
119
- if (notice !== undefined)
120
- context.ui.toast.show({ variant: notice.variant, title: notice.title, message: notice.message, duration: toastMs, sessionID });
121
- }
122
- sessions.set(sessionID, { view: next, motion: planMotion(entry?.view, next, now, entry?.motion ?? idleMotion, motionEnabled) });
299
+ const notice = leadingNotice(notices);
300
+ if (notice !== undefined)
301
+ context.ui.toast.show({ variant: notice.variant, title: notice.title, message: notice.message, duration: toastMs, sessionID });
302
+ const finish = notices.find((candidate) => candidate.celebrate === true);
303
+ if (finish !== undefined)
304
+ alertFinish(finish);
305
+ sessions.set(sessionID, next);
123
306
  setClock(now);
124
307
  setRevision(revision() + 1);
125
308
  ensureFrames();
309
+ syncBandTimer();
310
+ syncLiveTick();
311
+ syncPoll();
126
312
  }
127
313
  function syncPoll() {
128
314
  if (disposed)
@@ -141,6 +327,7 @@ export async function setupLitOpenCodeTui(context, runtime) {
141
327
  return;
142
328
  for (const sessionID of tracked)
143
329
  refresh(sessionID);
330
+ showServerNotices();
144
331
  syncPoll();
145
332
  }
146
333
  function scheduleRefresh() {
@@ -164,8 +351,13 @@ export async function setupLitOpenCodeTui(context, runtime) {
164
351
  const now = Date.now();
165
352
  trackedAt.set(sessionID, now);
166
353
  ignitions.set(sessionID, latestLitMarkV2(context.data, sessionID)?.messageID);
167
- sessions.set(sessionID, { view: viewFor(sessionID, now), motion: idleMotion });
168
- queueMicrotask(syncPoll);
354
+ suggestAdhd(sessionID, state());
355
+ sessions.set(sessionID, build(sessionID, undefined, now).next);
356
+ queueMicrotask(() => {
357
+ syncPoll();
358
+ syncLiveTick();
359
+ syncBandTimer();
360
+ });
169
361
  // Opening a session loads its messages over HTTP, which raises no event. Reading the host's reactive
170
362
  // store here re-runs this effect as they arrive, so a resumed session lights up too.
171
363
  const dispose = ui.createRoot((release) => {
@@ -184,6 +376,61 @@ export async function setupLitOpenCodeTui(context, runtime) {
184
376
  if (type.startsWith("session.") || type.startsWith("permission.") || type.startsWith("form."))
185
377
  scheduleRefresh();
186
378
  });
379
+ // Reads that only a rendered slot needs. `view` follows the reactive revision and the clock.
380
+ function snapshot(sessionID) {
381
+ revision();
382
+ const entry = sessions.get(sessionID);
383
+ const now = clock();
384
+ return entry === undefined ? undefined : { ...entry, frame: sampleMotion(entry.motion, now), now };
385
+ }
386
+ function statusCardRows(sessionID, inLane, columns) {
387
+ const current = snapshot(sessionID);
388
+ if (current === undefined)
389
+ return undefined;
390
+ const colors = palette();
391
+ const cells = columns - 2;
392
+ const last = current.view.active ? undefined : lastMission(readLedger(root));
393
+ const shimmer = chipShimmer(current.chipMotion, current.now);
394
+ const lines = statusCard({
395
+ view: current.view,
396
+ frame: current.frame,
397
+ chips: current.card.chips,
398
+ busy: current.card.busy,
399
+ motion: motionEnabled,
400
+ reserve: current.reserve,
401
+ ...(current.card.threads === undefined ? {} : { threads: current.card.threads }),
402
+ ...(current.card.earlier === true ? { earlier: true } : {}),
403
+ ...(current.waiting ? { turn: { glow: turnGlow(current.now, colorAllowed) } } : {}),
404
+ ...(rainbowOn(current, current.now) ? { rainbow: rainbowAt(current.now) } : {}),
405
+ ...(current.card.lanes !== undefined && activeSkillLook(current.view, current.card.chips, current.card.busy) === "review" ? { lanes: current.card.lanes } : {}),
406
+ ...(shimmer === undefined ? {} : { shimmer }),
407
+ figures: {
408
+ contextPercent: current.card.contextPercent,
409
+ ...(current.card.branch === undefined ? {} : { branch: current.card.branch }),
410
+ inLane,
411
+ ...(goSession(sessionID) && limits.windows().length > 0 ? { limits: limits.windows() } : {}),
412
+ ...(restartTo() === undefined ? {} : { restartTo: restartTo() })
413
+ },
414
+ ...(last === undefined ? {} : { last })
415
+ }, colors, cells);
416
+ const rows = lines.map(line);
417
+ const detail = band.card ? bandCard(current.view, current.frame, colors, cells) : undefined;
418
+ if (detail !== undefined) {
419
+ rows.push(ui.jsx("box", {
420
+ border: true,
421
+ borderStyle: "rounded",
422
+ borderColor: detail.border,
423
+ title: detail.title,
424
+ titleColor: colors.text,
425
+ paddingLeft: 1,
426
+ paddingRight: 1,
427
+ flexShrink: 0,
428
+ flexDirection: "column",
429
+ children: detail.rows.map(line)
430
+ }));
431
+ }
432
+ return rows;
433
+ }
187
434
  const releases = [
188
435
  context.ui.slot({
189
436
  append: "prompt.footer.status",
@@ -192,14 +439,30 @@ export async function setupLitOpenCodeTui(context, runtime) {
192
439
  if (sessionID === undefined)
193
440
  return null;
194
441
  track(sessionID);
442
+ const columns = width;
195
443
  return () => {
196
444
  revision();
197
445
  const mark = latestLitMarkV2(context.data, sessionID);
198
446
  if (mark === undefined)
199
447
  return null;
448
+ const current = snapshot(sessionID);
449
+ // The sidebar setting is not readable here, so a wide terminal is taken to have it open.
450
+ const sidebar = parentOf(sessionID) === undefined && columns() > sidebarMinWidth;
451
+ const footer = footerStatus({
452
+ name: mark.name,
453
+ view: current?.view,
454
+ frame: current?.frame ?? sampleMotion(idleMotion, clock()),
455
+ palette: palette(),
456
+ budget: footerBudget({ columns: columns(), sidebar, busy: current?.card.busy === true })
457
+ });
458
+ if (footer.length === 0)
459
+ return null;
200
460
  return ui.jsx("box", {
201
- flexShrink: 0,
202
- children: line([{ text: ignitionLine(mark.name), fg: palette().primary, bold: true }])
461
+ flexShrink: 1,
462
+ minWidth: 0,
463
+ overflow: "hidden",
464
+ flexDirection: "row",
465
+ children: line(footer)
203
466
  });
204
467
  };
205
468
  }
@@ -235,8 +498,52 @@ export async function setupLitOpenCodeTui(context, runtime) {
235
498
  });
236
499
  };
237
500
  }
501
+ }),
502
+ context.ui.slot({
503
+ append: "home.footer",
504
+ render() {
505
+ const columns = width;
506
+ return () => {
507
+ revision();
508
+ const segments = homeLine(lastMission(readLedger(root)), palette(), Math.min(homeLineWidth, columns() - 10));
509
+ return segments.length === 0 ? null : ui.jsx("box", { flexDirection: "row", justifyContent: "center", flexShrink: 0, children: line(segments) });
510
+ };
511
+ }
238
512
  })
239
513
  ];
514
+ // The card is drawn once for the whole app, under the session and its sidebar, on every session, lit or
515
+ // not, and keeps its rows when a turn starts. A subagent session shows its parent's card as a lane. The
516
+ // home route has no session, so it shows no card.
517
+ if (band.enabled) {
518
+ releases.push(context.ui.slot({
519
+ append: "app",
520
+ render() {
521
+ const columns = width;
522
+ return () => {
523
+ const route = context.ui.router?.current();
524
+ const sessionID = route?.type === "session" ? route.sessionID : undefined;
525
+ if (sessionID === undefined)
526
+ return null;
527
+ const parent = parentOf(sessionID);
528
+ const target = parent ?? sessionID;
529
+ track(target);
530
+ const rows = statusCardRows(target, parent !== undefined, columns());
531
+ if (rows === undefined)
532
+ return null;
533
+ return ui.jsx("box", {
534
+ flexDirection: "column",
535
+ flexShrink: 0,
536
+ paddingLeft: 1,
537
+ paddingRight: 1,
538
+ backgroundColor: palette().panel ?? "transparent",
539
+ children: rows
540
+ });
541
+ };
542
+ }
543
+ }));
544
+ }
545
+ void checkRestart();
546
+ restartTimer = setInterval(() => void checkRestart(), restartCheckMs);
240
547
  return async () => {
241
548
  disposed = true;
242
549
  for (const dispose of watchers.values())
@@ -246,12 +553,18 @@ export async function setupLitOpenCodeTui(context, runtime) {
246
553
  unlisten();
247
554
  for (const release of releases)
248
555
  release();
556
+ if (restartTimer !== undefined)
557
+ clearInterval(restartTimer);
249
558
  if (frameTimer !== undefined)
250
559
  clearInterval(frameTimer);
251
560
  if (refreshTimer !== undefined)
252
561
  clearTimeout(refreshTimer);
253
562
  if (pollTimer !== undefined)
254
563
  clearInterval(pollTimer);
564
+ if (liveTimer !== undefined)
565
+ clearInterval(liveTimer);
566
+ if (bandTimer !== undefined)
567
+ clearInterval(bandTimer);
255
568
  sessions.clear();
256
569
  ignitions.clear();
257
570
  trackedAt.clear();
@@ -0,0 +1,19 @@
1
+ export type LimitWindow = {
2
+ readonly label: "5h" | "1w" | "1mo";
3
+ readonly percent: number;
4
+ readonly limited: boolean;
5
+ };
6
+ /** Reads the usage-limits reply (`{ data: Info | null }` or the bare Info) into the windows worth drawing. */
7
+ export declare function limitWindows(reply: unknown): readonly LimitWindow[];
8
+ export type UsageLimitsOptions = {
9
+ /** Asks the host; absent when the host has no usage source. */
10
+ readonly read: (() => Promise<unknown>) | undefined;
11
+ readonly now: () => number;
12
+ readonly intervalMs: number;
13
+ readonly onChange: () => void;
14
+ };
15
+ /** A throttled reader: it asks at most once per interval, and a failed or empty answer clears the figures. */
16
+ export declare function createUsageLimits(options: UsageLimitsOptions): {
17
+ readonly windows: () => readonly LimitWindow[];
18
+ readonly poll: () => Promise<void>;
19
+ };
@@ -0,0 +1,70 @@
1
+ // The plan windows OpenCode 2 can tell a plugin about. Its one usage source is client.usage.limits(): the
2
+ // OpenCode Go allowance of the Console workspace the location is signed in to, as a rolling window (five
3
+ // hours), a weekly one and a monthly one, each with a percent used; the reply is null when nobody is signed
4
+ // in. There are no provider response headers or rate-limit events for a plugin to read, so for any other
5
+ // provider this module has nothing to show, and the card draws no figure rather than a made-up one.
6
+ /** The monthly window is shown only when it is close to the cap, so the card keeps LitClaude's 5h and 1w pair. */
7
+ const monthlyShowPercent = 90;
8
+ function record(value) {
9
+ return typeof value === "object" && value !== null ? value : undefined;
10
+ }
11
+ function window(label, raw) {
12
+ const meter = record(raw);
13
+ const percent = meter?.percent;
14
+ if (typeof percent !== "number" || !Number.isFinite(percent))
15
+ return undefined;
16
+ return { label, percent: Math.round(Math.min(100, Math.max(0, percent))), limited: meter?.limited === true };
17
+ }
18
+ /** Reads the usage-limits reply (`{ data: Info | null }` or the bare Info) into the windows worth drawing. */
19
+ export function limitWindows(reply) {
20
+ const outer = record(reply);
21
+ const info = outer === undefined ? undefined : "data" in outer ? record(outer.data) : outer;
22
+ const allowance = record(info?.go);
23
+ if (allowance === undefined)
24
+ return [];
25
+ const rolling = window("5h", allowance.rolling);
26
+ const weekly = window("1w", allowance.weekly);
27
+ const monthly = window("1mo", allowance.monthly);
28
+ return [
29
+ ...(rolling === undefined ? [] : [rolling]),
30
+ ...(weekly === undefined ? [] : [weekly]),
31
+ ...(monthly !== undefined && (monthly.limited || monthly.percent >= monthlyShowPercent) ? [monthly] : [])
32
+ ];
33
+ }
34
+ /** A throttled reader: it asks at most once per interval, and a failed or empty answer clears the figures. */
35
+ export function createUsageLimits(options) {
36
+ let current = [];
37
+ let askedAt;
38
+ let pending;
39
+ async function ask(read) {
40
+ let next;
41
+ try {
42
+ next = limitWindows(await read());
43
+ }
44
+ catch { // no-excuse-ok: catch -- an unreachable or signed-out host means there is nothing to show, never an error on screen.
45
+ next = [];
46
+ }
47
+ if (JSON.stringify(next) === JSON.stringify(current))
48
+ return;
49
+ current = next;
50
+ options.onChange();
51
+ }
52
+ return {
53
+ windows: () => current,
54
+ poll() {
55
+ const read = options.read;
56
+ if (read === undefined)
57
+ return Promise.resolve();
58
+ if (pending !== undefined)
59
+ return pending;
60
+ const now = options.now();
61
+ if (askedAt !== undefined && now - askedAt < options.intervalMs)
62
+ return Promise.resolve();
63
+ askedAt = now;
64
+ pending = ask(read).finally(() => {
65
+ pending = undefined;
66
+ });
67
+ return pending;
68
+ }
69
+ };
70
+ }
@@ -1 +1 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.36"><title>release: 1.0.36</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.36</text></g></svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.38"><title>release: 1.0.38</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.38</text></g></svg>
@@ -380,10 +380,29 @@ Code Mode ๋ฐ–, ๋ชจ๋ธ์˜ ๋„๊ตฌ ๋ชฉ๋ก์— ๋ฐ”๋กœ ์žˆ์œผ๋ฏ€๋กœ `lit-plan`๋„ ๊ณ„
380
380
  lit ํ”„๋กฌํ”„ํŠธ์ด๋ฉด ํ”„๋กฌํ”„ํŠธ ์•„๋ž˜ ์ค„(`prompt.footer.status`)์— `๐Ÿ”ฅ LIT IGNITED ยท <discipline>`์ด ๋‚˜์˜ต๋‹ˆ๋‹ค.
381
381
  OpenCode 1์—์„œ๋Š” ํ‘œ์‹œ๊ฐ€ ์˜ˆ์ „ ์ž๋ฆฌ์— ๊ทธ๋Œ€๋กœ ์žˆ์Šต๋‹ˆ๋‹ค.
382
382
  - *ํ† ์ŠคํŠธ.* OpenCode 2์—์„œ๋Š” ์„œ๋ฒ„๊ฐ€ ํ† ์ŠคํŠธ๋ฅผ ๋„์šธ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„๊ฐ€ ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์— ๋‚จ๊ธด lit ํ‘œ์‹œ๋ฅผ
383
- TUI๊ฐ€ ๋ณด๊ณ  ์ ํ™” ํ† ์ŠคํŠธ๋ฅผ ์ง์ ‘ ๋„์›๋‹ˆ๋‹ค. ์„œ๋ฒ„๋งŒ ์•Œ๋˜ ์ž๋™ ์ธ์ˆ˜์ธ๊ณ„ ์•Œ๋ฆผ๊ณผ ์Šคํ‚ฌ ํžŒํŠธ๋Š” ๊ทธ๊ณณ์— ๋‚˜์˜ค์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
384
- - *TUI.* ๋ฏธ์…˜ ํŒจ๋„์€ ์ „์ฒ˜๋Ÿผ ์„ธ์…˜ ์‚ฌ์ด๋“œ๋ฐ”์— ์žˆ์Šต๋‹ˆ๋‹ค. OpenCode 2๋Š” ์„œ๋ฒ„ ํ”Œ๋Ÿฌ๊ทธ์ธ๊ณผ ํ•จ๊ป˜ ์˜ค๋Š” TUI์— ์˜ต์…˜์„
385
- ๋„˜๊ธฐ์ง€ ์•Š๊ณ  ํ•  ์ผ ๋ชฉ๋ก๋„ ๋‘์ง€ ์•Š์œผ๋ฏ€๋กœ, ๋ฏธ์…˜ ์ง„ํ–‰์€ ledger์™€ ์ž‘์—… ๊ธฐ๋ก๊ณผ ๊ณ„ํš์—์„œ ์ฝ์Šต๋‹ˆ๋‹ค. ์„ธ์…˜ ์•„๋ž˜ ์ƒํƒœ ๋ ์™€
386
- ํ™ˆ ํ™”๋ฉด ํ•œ ์ค„์€ OpenCode 1์—์„œ๋งŒ ๊ทธ๋ฆฝ๋‹ˆ๋‹ค.
383
+ TUI๊ฐ€ ๋ณด๊ณ  ์ ํ™” ํ† ์ŠคํŠธ๋ฅผ ์ง์ ‘ ๋„์šฐ๊ณ , OpenCode 1๊ณผ ๊ฐ™์€ ๊ด€๋ฌธ ํ† ์ŠคํŠธ(๊ณ„ํš ์ €์žฅ, ๋ชฉํ‘œ ์„ค์ •, ๊ธฐ์ค€ ํ†ต๊ณผ, ๋ง‰ํž˜, ์™„๋ฃŒ)์—
384
+ ๋”ํ•ด OpenCode 2์—๋งŒ ์žˆ๋Š” ์„ธ ๊ฐ€์ง€๋ฅผ ๋„์›๋‹ˆ๋‹ค. ์ž‘์—… ๋‹จ๊ณ„๊ฐ€ ๋„˜์–ด๊ฐˆ ๋•Œ(`๐Ÿ”ฅ Stage`, `work 3/7`), ๊ทœ์œจ์ด ๋ฐ”๋€” ๋•Œ
385
+ (`๐Ÿ”ฅ lit-loop โ†’ start-work`), ๋ ˆ์ธ์ด ์‹œ์ž‘๋˜๊ฑฐ๋‚˜ ๋๋‚  ๋•Œ์ž…๋‹ˆ๋‹ค. ํ•œ๊บผ๋ฒˆ์— ์ผ์–ด๋‚œ ์ผ์€ `+N more`๊ฐ€ ๋ถ™์€ ํ† ์ŠคํŠธ
386
+ ํ•˜๋‚˜๋กœ ํ•ฉ์นฉ๋‹ˆ๋‹ค. ์žฌ์‹œ์ž‘ ์•ˆ๋‚ด, `lit adhd` ์ œ์•ˆ, ์ผœ ๋‘๋ฉด ๋‚˜์˜ค๋Š” ์™„๋ฃŒ ์•Œ๋ฆผ์€ OpenCode 1๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„๋งŒ ์•Œ๋˜
387
+ ์ž๋™ ์ธ์ˆ˜์ธ๊ณ„ ์•Œ๋ฆผ์€ `.litopencode/notices.jsonl`์„ ๊ฑฐ์ณ ํ™”๋ฉด์— ๋‚˜์˜ต๋‹ˆ๋‹ค. OpenCode 2 ์„œ๋ฒ„ ํ”Œ๋Ÿฌ๊ทธ์ธ์€ ์ด๋ฒคํŠธ๋ฅผ
388
+ ๊ตฌ๋…ํ•  ์ˆ˜๋งŒ ์žˆ๊ณ  ํ† ์ŠคํŠธ๋ฅผ ๋„์šธ ์ˆ˜ ์—†์œผ๋ฏ€๋กœ, ์„œ๋ฒ„๊ฐ€ ์•Œ๋ฆผ์„ ๊ทธ ํŒŒ์ผ์— ํ•œ ์ค„์”ฉ ๋ง๋ถ™์ด๋ฉด TUI ํ”Œ๋Ÿฌ๊ทธ์ธ์ด ์‹œ์ž‘ํ•œ ๋’ค์—
389
+ ์ƒ๊ธด ์ค„์„ ํ† ์ŠคํŠธ๋กœ ๋ณด์—ฌ ์ค๋‹ˆ๋‹ค(ํŒŒ์ผ ํฌ๊ธฐ์— ์ƒํ•œ์ด ์žˆ๊ณ  ์‹ฌ๋ณผ๋ฆญ ๋งํฌ๋Š” ๋”ฐ๋ผ๊ฐ€์ง€ ์•Š์Šต๋‹ˆ๋‹ค). ์„œ๋ฒ„๊ฐ€ ๋„์šฐ๋˜ ๋‚˜๋จธ์ง€
390
+ ํ•˜๋‚˜์ธ ์ ํ™” ํ† ์ŠคํŠธ๋Š” ์ด๋ฏธ TUI๊ฐ€ ์ง์ ‘ ๋„์šฐ๋ฏ€๋กœ, ๋”ฐ๋กœ ์˜ฎ๊ธธ ์Šคํ‚ฌ ํžŒํŠธ๋Š” ๋‚จ์•„ ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
391
+ - *TUI.* HUD๋Š” OpenCode 2๊ฐ€ ๊ณต๊ฐœํ•œ ์Šฌ๋กฏ์— ๋†“์ž…๋‹ˆ๋‹ค. ์ƒํƒœ ์นด๋“œ๋Š” `app` ์Šฌ๋กฏ์—์„œ ์„ธ์…˜๊ณผ ์‚ฌ์ด๋“œ๋ฐ” ์•„๋ž˜์— ๊ทธ๋ ค์ง€๊ณ ,
392
+ ์‰ฌ๋“  ์ผํ•˜๋“  ๋„ค ์ค„์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ๋‹จ๊ณ„ ์นฉ์€ ์—์ด์ „ํŠธยท๋ชจ๋ธ ์ค„ ์˜ค๋ฅธ์ชฝ์— ์Šฌ๋กฏ์ด ์—†์–ด์„œ ์ ํ™” ์ค„๊ณผ ํ•จ๊ป˜
393
+ `prompt.footer.status`์— ์žˆ๊ณ , ํ™ˆ ์ค„์€ `home.footer`, ๋ฏธ์…˜ ํŒจ๋„์€ `sidebar.content`์— ์žˆ์œผ๋ฏ€๋กœ ์‚ฌ์ด๋“œ๋ฐ”๋ฅผ ์—ด์–ด
394
+ ๋‘๋ฉด ๋ณด์ž…๋‹ˆ๋‹ค. ์นด๋“œ, ์นฉ, ํŒจ๋„, ํ™ˆ ์ค„์€ OpenCode 1๊ณผ ๊ฐ™์€ ๊ธ€์ž ๋นŒ๋”๋ฅผ ์”๋‹ˆ๋‹ค. ํ”Œ๋Ÿฌ๊ทธ์ธ ํ•ญ๋ชฉ์˜ `band`, `notify`
395
+ ์˜ต์…˜์€ OpenCode 2๊ฐ€ ๋„˜๊ฒจ ์ฃผ๋Š” ์˜ต์…˜์—์„œ ์ฝ์Šต๋‹ˆ๋‹ค. ํ‘ธํ„ฐ ์ค„์€ ํ˜ธ์ŠคํŠธ๊ฐ€ ์ž˜๋ผ ๋ฒ„๋ฆฌ๊ฒŒ ๋‘์ง€ ์•Š๊ณ  ํ„ฐ๋ฏธ๋„ ๋„ˆ๋น„์— ๋งž์ถฐ
396
+ ๋‹จ๊ณ„์ ์œผ๋กœ ์ค„์–ด๋“ญ๋‹ˆ๋‹ค. ํ‘œ์‹œ์™€ ์ „์ฒด ์นฉ, ํ‘œ์‹œ์™€ ์ž‘์€ ์นฉ, ์นฉ๋งŒ, `๐Ÿ”ฅ 1/3`, ๋ถˆ๊ฝƒ๋งŒ, ์•„๋ฌด๊ฒƒ๋„ ์—†์Œ ์ˆœ์„œ์ž…๋‹ˆ๋‹ค. ์“ธ ์ˆ˜ ์žˆ๋Š”
397
+ ๋„ˆ๋น„๋Š” ์ „์ฒด ๋„ˆ๋น„์—์„œ ์‚ฌ์ด๋“œ๋ฐ”(120์นธ์„ ๋„˜์œผ๋ฉด ์—ด๋ ค ์žˆ๋‹ค๊ณ  ๊ฐ€์ •)์™€ ํ˜ธ์ŠคํŠธ ์ž์‹ ์˜ ์Šคํ”ผ๋„ˆยท์ˆ˜์น˜๋ฅผ ๋บ€ ๊ฐ’์ž…๋‹ˆ๋‹ค.
398
+ OpenCode 2๋Š” ํ•  ์ผ ๋ชฉ๋ก์„ ๋‘์ง€ ์•Š์œผ๋ฏ€๋กœ ๋ฏธ์…˜ ์ง„ํ–‰์€ ledger์™€
399
+ ์ž‘์—… ๊ธฐ๋ก๊ณผ ๊ณ„ํš์—์„œ ์ฝ์Šต๋‹ˆ๋‹ค. ๋„๊ตฌ ์ด๋ฆ„๋„ ๋‹ค๋ฆ…๋‹ˆ๋‹ค(`shell`, `subagent`, `id`๋ฅผ ๋ฐ›๋Š” `skill` ๋„๊ตฌ). ์นด๋“œ๋Š” ๊ทธ
400
+ ์ด๋ฆ„์„ ์ฝ์Šต๋‹ˆ๋‹ค. OpenCode 2๊ฐ€ ํ”Œ๋Ÿฌ๊ทธ์ธ์— ์ฃผ๋Š” ์‚ฌ์šฉ๋Ÿ‰ ์ถœ์ฒ˜๋Š” `client.usage.limits()` ํ•˜๋‚˜์ž…๋‹ˆ๋‹ค. ์œ„์น˜๊ฐ€ ๋กœ๊ทธ์ธํ•œ
401
+ OpenCode Console ์ž‘์—… ๊ณต๊ฐ„์˜ OpenCode Go ํ—ˆ์šฉ๋Ÿ‰(5์‹œ๊ฐ„ ๋กค๋ง, ์ฃผ๊ฐ„, ์›”๊ฐ„ ์ฐฝ)์ž…๋‹ˆ๋‹ค. ๋งˆ์ง€๋ง‰ ๋‹ต๋ณ€์ด `opencode-go`
402
+ ์ œ๊ณต์ž์—์„œ ์˜จ ์„ธ์…˜์ด๋ฉด ์นด๋“œ ์•„๋ž˜ ํ…Œ๋‘๋ฆฌ์— ์ปจํ…์ŠคํŠธ ์ˆ˜์น˜ ๋’ค๋กœ `5h 42% ยท 1w 18%`๊ฐ€ ๋‚˜์˜ต๋‹ˆ๋‹ค(์›”๊ฐ„ ์ฐฝ์€ 90% ์ด์ƒ์ผ
403
+ ๋•Œ๋งŒ, ํ˜ธ์ŠคํŠธ๊ฐ€ ์š”์ฒญ์„ ๊ฑฐ๋ถ€ ์ค‘์ด๋ผ๊ณ  ์•Œ๋ฆฌ๋Š” ์ฐฝ์€ `limited`๋กœ ํ‘œ์‹œ). ํ˜ธ์ถœ์€ 1๋ถ„์— ํ•œ ๋ฒˆ ์ดํ•˜์ž…๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ์ œ๊ณต์ž,
404
+ ๋กœ๊ทธ์•„์›ƒ ์ƒํƒœ(์‘๋‹ต์ด `null`), ์˜ค๋ฅ˜์ผ ๋•Œ๋Š” ์•„๋ฌด๊ฒƒ๋„ ๊ทธ๋ฆฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. OpenCode 2๋Š” ์ œ๊ณต์ž ์†๋„ ์ œํ•œ ํ—ค๋”๋‚˜ ์ด๋ฒคํŠธ๋ฅผ
405
+ ํ”Œ๋Ÿฌ๊ทธ์ธ์— ๋…ธ์ถœํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ AnthropicยทOpenAI ์š”๊ธˆ์ œ์˜ 5์‹œ๊ฐ„ยท์ฃผ๊ฐ„ ์ˆ˜์น˜๋Š” ์—†์Šต๋‹ˆ๋‹ค.
387
406
  - *์—…๋ฐ์ดํŠธ.* OpenCode 2์—์„œ๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ ์•ˆ์˜ ์ž๋™ ์—…๋ฐ์ดํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. `opencode plugin update`๋ฅผ ์‹คํ–‰ํ•˜๊ฑฐ๋‚˜
388
407
  OpenCode 2์˜ ์—…๋ฐ์ดํŠธ ๋ฐฉ์‹์— ๋งก๊ธฐ๋ฉด ๋ฉ๋‹ˆ๋‹ค.
389
408
 
@@ -458,7 +477,7 @@ OpenCode 1.18์€ `tui.json`์ด๋‚˜ `tui.jsonc`์˜ `plugin` ๋ฐฐ์—ด์— ์ ํžŒ TUI
458
477
  - `lit-adhd`(๋ณ„์นญ `lit-rewind`)๊ฐ€ ๋„๋Š” ๋™์•ˆ ํ…Œ๋‘๋ฆฌ, LIT ๊ธ€์ž, ๊ฒŒ์ด์ง€๊ฐ€ ๊ฐ™์€ ๋ฐ๊ธฐ์˜ ์ƒ‰์ƒํ™˜์„ ๋”ฐ๋ผ ๋•๋‹ˆ๋‹ค.
459
478
  1์ดˆ์— ๋„ค ๋ฒˆ ์ƒ‰์ด ์กฐ๊ธˆ์”ฉ ๋ฐ”๋€Œ๊ณ  ๊นœ๋ฐ•์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. `motion: false`์™€ `NO_COLOR`์—์„œ๋Š” ๊บผ์ง‘๋‹ˆ๋‹ค. ์ด์ „
460
479
  ์ž‘์—…์ด ์žˆ๋Š” ์ž‘์—… ๊ณต๊ฐ„์˜ ์‰ฌ๋Š” ์„ธ์…˜์€ `lit adhd to pick up where you left off`๋ฅผ ๋ณด์—ฌ ์ฃผ๊ณ , ๊ทธ๊ณณ์—์„œ ์ฒ˜์Œ ์—ฐ
461
- ์ƒˆ ์„ธ์…˜์€ ๊ฐ™์€ ๋ฌธ๊ตฌ๋กœ ํ† ์ŠคํŠธ๋ฅผ ํ•œ ๋ฒˆ ๋„์›๋‹ˆ๋‹ค(v1 TUI๋งŒ. v2 TUI์—๋Š” ๋ ๋„ ํ† ์ŠคํŠธ๋„ ์—†์Šต๋‹ˆ๋‹ค).
480
+ ์ƒˆ ์„ธ์…˜์€ ๊ฐ™์€ ๋ฌธ๊ตฌ๋กœ ํ† ์ŠคํŠธ๋ฅผ ํ•œ ๋ฒˆ ๋„์›๋‹ˆ๋‹ค(OpenCode 1๊ณผ 2 ๋ชจ๋‘).
462
481
  - ์นด๋“œ์˜ ๋ชจ์–‘์€ ํ•œ ๋ฒˆ์— ํ•˜๋‚˜์ด๊ณ , `src/tui/mission-looks.ts`์˜ ์ž‘์€ ๋ชฉ๋ก์—์„œ ๊ณ ๋ฆ…๋‹ˆ๋‹ค. lit-adhd๋ฅผ ์‹คํ–‰ํ•˜๋Š”
463
482
  ๊ธธ์€ ๋ชจ๋‘ ๋˜๊ฐ๊ธฐ ๋ชจ์–‘์ด ๋ฉ๋‹ˆ๋‹ค. ์ฑ„ํŒ…์˜ `lit adhd`/`lit rewind`, `/lit-adhd`/`/lit-rewind`(v2์— ์„ค์น˜๋œ ๋ณ„์นญ
464
483
  ํŒŒ์ผ์—์„œ ์˜จ ๊ฒƒ๋„ ํฌํ•จ), ๋„ค์ดํ‹ฐ๋ธŒ `skill` ๋กœ๋“œ๊ฐ€ ๊ทธ๋ ‡์Šต๋‹ˆ๋‹ค. ์„ธ์…˜์ด ์‚ฌ์šฉ์ž๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๋Š” ๋™์•ˆ(๊ถŒํ•œ ์š”์ฒญ์ด๋‚˜ ์งˆ๋ฌธ,