@litfamily/litopencode 1.0.18 → 1.0.20

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 (56) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README-Ko-KR.md +56 -86
  3. package/README.md +57 -86
  4. package/dist/activation-routing.js +3 -1
  5. package/dist/cli/args.js +5 -1
  6. package/dist/cli/auto-update.js +1 -1
  7. package/dist/cli/doctor.js +7 -5
  8. package/dist/cli/host-limits.js +2 -5
  9. package/dist/cli/install-config.d.ts +4 -0
  10. package/dist/cli/install-config.js +27 -1
  11. package/dist/cli/install-report.js +4 -1
  12. package/dist/cli/install.js +89 -28
  13. package/dist/cli/json.d.ts +1 -0
  14. package/dist/cli/json.js +10 -4
  15. package/dist/cli/jsonc.d.ts +17 -0
  16. package/dist/cli/jsonc.js +174 -0
  17. package/dist/cli/loop.d.ts +1 -1
  18. package/dist/cli/loop.js +81 -1
  19. package/dist/cli/motion-runtime.d.ts +13 -1
  20. package/dist/cli/motion-runtime.js +89 -2
  21. package/dist/cli/plugin-mutation.js +11 -6
  22. package/dist/cli/tui-config.d.ts +52 -0
  23. package/dist/cli/tui-config.js +198 -0
  24. package/dist/cli/types.d.ts +4 -0
  25. package/dist/cli.js +8 -3
  26. package/dist/deliverable-hedge-guard.js +2 -2
  27. package/dist/tui/mission-band.d.ts +47 -0
  28. package/dist/tui/mission-band.js +290 -0
  29. package/dist/tui/mission-model.d.ts +139 -0
  30. package/dist/tui/mission-model.js +318 -0
  31. package/dist/tui/mission-motion.d.ts +66 -0
  32. package/dist/tui/mission-motion.js +147 -0
  33. package/dist/tui/mission-skills.d.ts +38 -0
  34. package/dist/tui/mission-skills.js +81 -0
  35. package/dist/tui/mission-source.d.ts +68 -0
  36. package/dist/tui/mission-source.js +112 -0
  37. package/dist/tui/mission-text.d.ts +106 -0
  38. package/dist/tui/mission-text.js +613 -0
  39. package/dist/tui.d.ts +6 -0
  40. package/dist/tui.js +342 -0
  41. package/docs/assets/readme/badge-version.svg +1 -1
  42. package/docs/reference-Ko-KR.md +91 -1
  43. package/docs/reference.md +100 -1
  44. package/package.json +21 -2
  45. package/skills/lit-pptx/scripts/lib/template-registry.js +1 -1
  46. package/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +0 -6
  47. package/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +0 -6
  48. package/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +0 -6
  49. package/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +0 -6
  50. package/skills/lit-pptx/templates/tonalities/night/pack.yaml +0 -6
  51. package/skills/lit-pptx/templates/tonalities/paper/pack.yaml +0 -6
  52. package/skills/lit-pptx/templates/tonalities/signal/pack.yaml +0 -6
  53. package/skills/lit-pptx/templates/tonalities/studio/pack.yaml +0 -6
  54. package/skills/managed-skill-manifest.json +9 -9
  55. package/tools/check-pack-payload.mjs +20 -5
  56. package/tools/version-manifests.json +18 -6
@@ -9,18 +9,23 @@ function pluginEntryId(value) {
9
9
  return value[0];
10
10
  return "";
11
11
  }
12
+ // The install target counts as a LitOpenCode entry even when it is a local checkout path, which the
13
+ // package-name match cannot recognize; otherwise a re-install would append the same path again.
14
+ function isRegistrationEntry(value, target) {
15
+ return isLitOpenCodeEntry(value) || pluginEntryId(value) === target;
16
+ }
12
17
  function replaceLitOpenCodeEntry(value, target) {
13
- if (Array.isArray(value) && isLitOpenCodeEntry(value))
18
+ if (Array.isArray(value))
14
19
  return [target, ...value.slice(1)];
15
20
  return target;
16
21
  }
17
22
  function normalizedPluginArray(pluginValue, target) {
18
- const firstLitEntry = pluginValue.find(isLitOpenCodeEntry);
23
+ const firstLitEntry = pluginValue.find((value) => isRegistrationEntry(value, target));
19
24
  const replacement = firstLitEntry === undefined ? target : replaceLitOpenCodeEntry(firstLitEntry, target);
20
25
  let replaced = false;
21
26
  const next = [];
22
27
  for (const value of pluginValue) {
23
- if (!isLitOpenCodeEntry(value)) {
28
+ if (!isRegistrationEntry(value, target)) {
24
29
  next.push(value);
25
30
  continue;
26
31
  }
@@ -34,9 +39,9 @@ function normalizedPluginArray(pluginValue, target) {
34
39
  export function describePluginMutation(config, target) {
35
40
  const pluginValue = config?.plugin;
36
41
  const pluginIsArray = Array.isArray(pluginValue);
37
- const existingIndex = pluginIsArray ? pluginValue.findIndex(isLitOpenCodeEntry) : -1;
42
+ const existingIndex = pluginIsArray ? pluginValue.findIndex((value) => isRegistrationEntry(value, target)) : -1;
38
43
  const targetIndex = pluginIsArray ? pluginValue.findIndex((value) => pluginEntryId(value) === target) : -1;
39
- const litEntryCount = pluginIsArray ? pluginValue.filter(isLitOpenCodeEntry).length : 0;
44
+ const litEntryCount = pluginIsArray ? pluginValue.filter((value) => isRegistrationEntry(value, target)).length : 0;
40
45
  const alreadyPresent = targetIndex >= 0 && litEntryCount === 1;
41
46
  const currentCount = pluginIsArray ? pluginValue.length : 0;
42
47
  if (alreadyPresent) {
@@ -81,7 +86,7 @@ export function applyPluginMutation(config, mutation, target) {
81
86
  return next;
82
87
  const pluginValue = next.plugin;
83
88
  if (Array.isArray(pluginValue)) {
84
- next.plugin = pluginValue.some(isLitOpenCodeEntry) ? normalizedPluginArray(pluginValue, target) : [...pluginValue, target];
89
+ next.plugin = pluginValue.some((value) => isRegistrationEntry(value, target)) ? normalizedPluginArray(pluginValue, target) : [...pluginValue, target];
85
90
  return next;
86
91
  }
87
92
  next.plugin = [target];
@@ -0,0 +1,52 @@
1
+ export declare const tuiSchemaUrl = "https://opencode.ai/tui.json";
2
+ export declare const tuiBackupSuffix = ".litopencode-backup";
3
+ type PlannedWrite = {
4
+ readonly path: string;
5
+ readonly before: string | null;
6
+ readonly text: string;
7
+ };
8
+ export type TuiInstallReport = {
9
+ readonly path: string;
10
+ readonly alreadyPresent: boolean;
11
+ readonly changed: boolean;
12
+ readonly add: readonly string[];
13
+ readonly currentCount: number;
14
+ readonly resultCount: number;
15
+ readonly cleaned: readonly string[];
16
+ };
17
+ export type TuiInstallPlan = {
18
+ readonly report: TuiInstallReport;
19
+ readonly writes: readonly PlannedWrite[];
20
+ readonly blocked?: string;
21
+ };
22
+ export type TuiRemovalReport = {
23
+ readonly dryRun: boolean;
24
+ readonly removed: number;
25
+ readonly files: readonly {
26
+ readonly path: string;
27
+ readonly removed: number;
28
+ readonly backup?: string;
29
+ }[];
30
+ };
31
+ export declare function tuiConfigCandidates(root: string): readonly [string, string];
32
+ /** Works out every TUI config edit an install makes, without writing. A comment that would be lost blocks the plan. */
33
+ export declare function planTuiInstall(root: string, target: string): Promise<TuiInstallPlan>;
34
+ /** Writes a plan: each file that already existed is copied to `<file>.litopencode-backup` first. */
35
+ export declare function applyTuiWrites(writes: readonly PlannedWrite[], options?: {
36
+ readonly backup?: boolean;
37
+ }): Promise<void>;
38
+ /** Backs up the TUI files a plan is about to change, for writers outside LitOpenCode such as `opencode plugin`. */
39
+ export declare function backupTuiFiles(plan: TuiInstallPlan): Promise<void>;
40
+ /** Removes LitOpenCode's own TUI entries, and only those, from tui.json and tui.jsonc. */
41
+ export declare function removeTuiPlugin(root: string, dryRun: boolean): Promise<TuiRemovalReport>;
42
+ export type TuiInspection = {
43
+ readonly path: string;
44
+ readonly expected: string;
45
+ readonly configured: string | null;
46
+ readonly present: boolean;
47
+ readonly entries: number;
48
+ readonly ok: boolean;
49
+ readonly error?: string;
50
+ };
51
+ export declare function inspectTuiPlugin(root: string, expected: string): Promise<TuiInspection>;
52
+ export {};
@@ -0,0 +1,198 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { OpenCodeConfigCommentError, renderOpenCodeConfig, writeTextAtomically } from "./install-config.js";
4
+ import { isRecord, readJsonObjectIfPresent, readTextIfPresent } from "./json.js";
5
+ import { parseJsonc } from "./jsonc.js";
6
+ import { isLitOpenCodeEntry } from "./plugin-mutation.js";
7
+ // OpenCode reads TUI plugins from the `plugin` list of tui.json and tui.jsonc beside opencode.json,
8
+ // merging both. When it adds an entry itself it edits the first of the two that exists, else creates
9
+ // tui.json; LitOpenCode follows the same choice so a host install and ours never split the entry.
10
+ export const tuiSchemaUrl = "https://opencode.ai/tui.json";
11
+ export const tuiBackupSuffix = ".litopencode-backup";
12
+ export function tuiConfigCandidates(root) {
13
+ const resolved = path.resolve(root);
14
+ return [path.join(resolved, "tui.json"), path.join(resolved, "tui.jsonc")];
15
+ }
16
+ function entryId(value) {
17
+ if (typeof value === "string")
18
+ return value;
19
+ if (Array.isArray(value) && typeof value[0] === "string")
20
+ return value[0];
21
+ return "";
22
+ }
23
+ async function isLitOpenCodePathEntry(root, id) {
24
+ if (!(path.isAbsolute(id) || id.startsWith(".")))
25
+ return false;
26
+ const metadata = await readJsonObjectIfPresent(path.join(path.resolve(root, id), "package.json")).catch(() => null);
27
+ return metadata?.name === "litopencode" || metadata?.name === "@litfamily/opencode" || metadata?.name === "@litfamily/litopencode";
28
+ }
29
+ async function isOwnEntry(root, value, target) {
30
+ const id = entryId(value);
31
+ return id !== "" && (id === target || isLitOpenCodeEntry(value) || (await isLitOpenCodePathEntry(root, id)));
32
+ }
33
+ async function readTuiFile(file) {
34
+ const text = await readTextIfPresent(file);
35
+ if (text === null)
36
+ return { path: file, text, config: null };
37
+ let parsed;
38
+ try {
39
+ parsed = parseJsonc(text);
40
+ }
41
+ catch (error) {
42
+ throw new Error(`Malformed JSONC at ${file}: ${error instanceof Error ? error.message : String(error)}. Nothing was written.`);
43
+ }
44
+ if (!isRecord(parsed))
45
+ throw new Error(`Malformed JSON at ${file}: expected an object. Nothing was written.`);
46
+ return { path: file, text, config: parsed };
47
+ }
48
+ async function ownIndexes(root, plugins, target) {
49
+ const indexes = [];
50
+ for (const [index, value] of plugins.entries())
51
+ if (await isOwnEntry(root, value, target))
52
+ indexes.push(index);
53
+ return indexes;
54
+ }
55
+ function pluginList(config) {
56
+ return Array.isArray(config?.plugin) ? config.plugin : [];
57
+ }
58
+ function render(file, next) {
59
+ return { path: file.path, before: file.text, text: renderOpenCodeConfig(file.path, file.text, next) };
60
+ }
61
+ /** Works out every TUI config edit an install makes, without writing. A comment that would be lost blocks the plan. */
62
+ export async function planTuiInstall(root, target) {
63
+ const files = await Promise.all(tuiConfigCandidates(root).map(readTuiFile));
64
+ const owned = await Promise.all(files.map(async (file) => ownIndexes(root, pluginList(file.config), target)));
65
+ const holder = files.findIndex((_file, index) => owned[index].length > 0);
66
+ const primaryIndex = holder >= 0 ? holder : Math.max(0, files.findIndex((file) => file.text !== null));
67
+ const primary = files[primaryIndex];
68
+ const current = pluginList(primary.config);
69
+ const primaryOwned = owned[primaryIndex];
70
+ const alreadyPresent = primaryOwned.length === 1 && entryId(current[primaryOwned[0]]) === target;
71
+ let nextPlugins;
72
+ if (primaryOwned.length === 0) {
73
+ nextPlugins = [...current, target];
74
+ }
75
+ else {
76
+ const first = current[primaryOwned[0]];
77
+ const replacement = Array.isArray(first) ? [target, ...first.slice(1)] : target;
78
+ nextPlugins = current.flatMap((value, index) => (index === primaryOwned[0] ? [replacement] : primaryOwned.includes(index) ? [] : [value]));
79
+ }
80
+ const writes = [];
81
+ const cleaned = [];
82
+ try {
83
+ if (JSON.stringify(nextPlugins) !== JSON.stringify(current) || primary.config === null) {
84
+ const base = primary.config === null ? { $schema: tuiSchemaUrl } : primary.config;
85
+ writes.push(render(primary, { ...base, plugin: nextPlugins }));
86
+ }
87
+ for (const [index, file] of files.entries()) {
88
+ if (index === primaryIndex || owned[index].length === 0 || file.config === null)
89
+ continue;
90
+ const kept = pluginList(file.config).filter((_value, position) => !owned[index].includes(position));
91
+ writes.push(render(file, { ...file.config, plugin: kept }));
92
+ cleaned.push(file.path);
93
+ }
94
+ }
95
+ catch (error) {
96
+ if (!(error instanceof OpenCodeConfigCommentError))
97
+ throw error;
98
+ return { report: { ...report(), changed: true, add: [target] }, writes: [], blocked: error.message };
99
+ }
100
+ return { report: report(), writes };
101
+ function report() {
102
+ const changed = writes.length > 0;
103
+ return {
104
+ path: primary.path,
105
+ alreadyPresent,
106
+ changed,
107
+ add: changed ? [target] : [],
108
+ currentCount: current.length,
109
+ resultCount: nextPlugins.length,
110
+ cleaned
111
+ };
112
+ }
113
+ }
114
+ async function backup(write) {
115
+ if (write.before === null)
116
+ return undefined;
117
+ const file = write.path + tuiBackupSuffix;
118
+ await writeTextAtomically(file, write.before);
119
+ return file;
120
+ }
121
+ /** Writes a plan: each file that already existed is copied to `<file>.litopencode-backup` first. */
122
+ export async function applyTuiWrites(writes, options = {}) {
123
+ for (const write of writes) {
124
+ if (options.backup !== false)
125
+ await backup(write);
126
+ await fs.mkdir(path.dirname(write.path), { recursive: true });
127
+ await writeTextAtomically(write.path, write.text);
128
+ }
129
+ }
130
+ /** Backs up the TUI files a plan is about to change, for writers outside LitOpenCode such as `opencode plugin`. */
131
+ export async function backupTuiFiles(plan) {
132
+ for (const write of plan.writes)
133
+ await backup(write);
134
+ }
135
+ /** Removes LitOpenCode's own TUI entries, and only those, from tui.json and tui.jsonc. */
136
+ export async function removeTuiPlugin(root, dryRun) {
137
+ const files = await Promise.all(tuiConfigCandidates(root).map(readTuiFile));
138
+ const writes = [];
139
+ for (const file of files) {
140
+ if (file.config === null)
141
+ continue;
142
+ const plugins = pluginList(file.config);
143
+ const owned = await ownIndexes(root, plugins, undefined);
144
+ if (owned.length === 0)
145
+ continue;
146
+ const kept = plugins.filter((_value, index) => !owned.includes(index));
147
+ writes.push({ write: render(file, { ...file.config, plugin: kept }), removed: owned.length });
148
+ }
149
+ const reports = [];
150
+ for (const { write, removed } of writes) {
151
+ if (dryRun) {
152
+ reports.push({ path: write.path, removed });
153
+ continue;
154
+ }
155
+ const backupPath = await backup(write);
156
+ await writeTextAtomically(write.path, write.text);
157
+ reports.push({ path: write.path, removed, ...(backupPath === undefined ? {} : { backup: backupPath }) });
158
+ }
159
+ return { dryRun, removed: reports.reduce((total, entry) => total + entry.removed, 0), files: reports };
160
+ }
161
+ export async function inspectTuiPlugin(root, expected) {
162
+ let files;
163
+ try {
164
+ files = await Promise.all(tuiConfigCandidates(root).map(readTuiFile));
165
+ }
166
+ catch (error) {
167
+ const message = error instanceof Error ? error.message : String(error);
168
+ return { path: tuiConfigCandidates(root)[0], expected, configured: null, present: false, entries: 0, ok: false, error: message };
169
+ }
170
+ let found;
171
+ let entries = 0;
172
+ let present = false;
173
+ for (const file of files) {
174
+ for (const value of pluginList(file.config)) {
175
+ const id = entryId(value);
176
+ const exact = id === expected || (await isLitOpenCodePathEntry(root, id));
177
+ if (!exact && !isLitOpenCodeEntry(value))
178
+ continue;
179
+ entries += 1;
180
+ if (exact && !present) {
181
+ present = true;
182
+ found = { path: file.path, id };
183
+ }
184
+ else if (found === undefined) {
185
+ found = { path: file.path, id };
186
+ }
187
+ }
188
+ }
189
+ const fallback = files.find((file) => file.text !== null)?.path ?? files[0].path;
190
+ return {
191
+ path: found?.path ?? fallback,
192
+ expected,
193
+ configured: found?.id ?? null,
194
+ present,
195
+ entries,
196
+ ok: present && entries === 1
197
+ };
198
+ }
@@ -1,4 +1,5 @@
1
1
  import type { OpenCodeHostCapabilities } from "./host-capabilities.ts";
2
+ import type { TuiInstallReport } from "./tui-config.ts";
2
3
  export type CliResult = {
3
4
  readonly exitCode: number;
4
5
  readonly stdout?: string;
@@ -25,6 +26,7 @@ export type ParsedArgs = {
25
26
  readonly modelPrompt: ModelPromptMode;
26
27
  readonly outputStylePrompt: PermissionPromptMode;
27
28
  readonly loop: LoopArgs;
29
+ readonly tuiAction?: string;
28
30
  };
29
31
  export type LoopArgs = {
30
32
  readonly projectRoot: string;
@@ -181,8 +183,10 @@ export type InstallReport = {
181
183
  readonly compactionTriggerTokens: number;
182
184
  readonly compactionReserveTokens: number;
183
185
  };
186
+ readonly tui: TuiInstallReport;
184
187
  readonly patch: readonly JsonPatchOperation[];
185
188
  readonly changed: boolean;
189
+ readonly blocked?: string;
186
190
  readonly routes?: InstallRouteReport;
187
191
  readonly capabilities: OpenCodeHostCapabilities;
188
192
  readonly package: Pick<PackageMetadata, "name" | "version">;
package/dist/cli.js CHANGED
@@ -10,6 +10,7 @@ import { defaultOpenAiRoutes, legacyFastAliasHelp, modelEffortHelp, openaiModelH
10
10
  import { fetchPublicSource } from "./lit-fetch.js";
11
11
  import { readPackageMetadata } from "./cli/json.js";
12
12
  import { runMotionRuntime } from "./cli/motion-runtime.js";
13
+ import { removeTuiPlugin } from "./cli/tui-config.js";
13
14
  import path from "node:path";
14
15
  import { pathToFileURL } from "node:url";
15
16
  function helpText() {
@@ -34,7 +35,8 @@ function helpText() {
34
35
  " litopencode install [--permission-prompt | --no-permission-prompt] [--permission-mode <safe|balanced|yolo>]",
35
36
  " litopencode install|doctor [--no-auto-update]",
36
37
  " litopencode doctor [--json] [--root <dir>]",
37
- " litopencode motion-runtime install|status [--audio] [--word-timing]",
38
+ " litopencode tui remove [--dry-run] [--root <dir>]",
39
+ " litopencode motion-runtime install|status [--audio] [--word-timing] [--dry-run] (install) [--json] (status)",
38
40
  " litopencode fetch-public <url> [--json] [--timeout <ms>] [--max-bytes <n>]",
39
41
  " litopencode fetch-public <local-url> --json --allow-private-network # trusted fixtures only",
40
42
  " OpenCode /start-work init|resume|cancel|complete|status <schema-3 JSON>",
@@ -52,6 +54,7 @@ function helpText() {
52
54
  "Commands:",
53
55
  " install Register litopencode with OpenCode using a branded installer UI.",
54
56
  " doctor Report package, config, and runtime path status without writing files.",
57
+ " tui remove Remove only LitOpenCode's TUI panel entry from tui.json/tui.jsonc (backed up first).",
55
58
  " fetch-public Fetch a public URL with SSRF guards, verdicts, and trace output.",
56
59
  " --allow-private-network is opt-in for reviewed local fixtures or trusted internal tests.",
57
60
  "",
@@ -118,7 +121,7 @@ function helpText() {
118
121
  export async function runCli(argv = process.argv.slice(2), options = {}) {
119
122
  if (argv[0] === "motion-runtime") {
120
123
  const metadata = await readPackageMetadata();
121
- return runMotionRuntime(metadata.packageRoot, argv.slice(1));
124
+ return await runMotionRuntime(metadata.packageRoot, argv.slice(1));
122
125
  }
123
126
  let parsed;
124
127
  try {
@@ -133,9 +136,11 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
133
136
  return { exitCode: 0, stdout: helpText() };
134
137
  }
135
138
  if (isLoopCommand(parsed.command))
136
- return await runLoopCommand(parsed.command, parsed.loop);
139
+ return await runLoopCommand(parsed.command, parsed.loop, parsed.dryRun);
137
140
  if (parsed.command === "doctor")
138
141
  return await doctor(parsed.root);
142
+ if (parsed.command === "tui")
143
+ return { exitCode: 0, stdout: JSON.stringify(await removeTuiPlugin(parsed.root, parsed.dryRun), null, 2) };
139
144
  if (parsed.command === "fetch-public") {
140
145
  if (!parsed.fetchPublicUrl)
141
146
  return { exitCode: 2, stderr: "fetch-public requires a URL" };
@@ -8,9 +8,9 @@ const defaultTimeoutMs = 1500;
8
8
  const maxTextBytes = 4 * 1024 * 1024;
9
9
  const maxOutputBytes = 16 * 1024 * 1024;
10
10
  const maxPendingCalls = 512;
11
+ // Prose formats only: in config and data files (YAML, JSON, XML, CSV, TSV) a line such as `source:` is a key.
11
12
  const readerFacingExtensions = new Set([
12
- ".adoc", ".csv", ".html", ".htm", ".md", ".mdx", ".org", ".rst", ".svg", ".tex", ".txt", ".tsv",
13
- ".xhtml", ".xml", ".json", ".jsonc", ".yaml", ".yml"
13
+ ".adoc", ".html", ".htm", ".md", ".mdx", ".org", ".rst", ".svg", ".tex", ".txt", ".xhtml"
14
14
  ]);
15
15
  const officeExtensions = new Set([".docx", ".pptx", ".pdf"]);
16
16
  const packageSkillRoot = fileURLToPath(new URL("../skills/lit-humanizer", import.meta.url));
@@ -0,0 +1,47 @@
1
+ import { type MotionFrame } from "./mission-motion.ts";
2
+ import { type MissionView } from "./mission-model.ts";
3
+ import type { ChipShimmer, SkillChip } from "./mission-skills.ts";
4
+ import { type LastMission, type Line, type MissionPalette } from "./mission-text.ts";
5
+ export type BandOptions = {
6
+ readonly enabled: boolean;
7
+ readonly card: boolean;
8
+ };
9
+ /**
10
+ * Reads the band switches from the plugin's tui.json options: `band: false` turns the band off,
11
+ * `band: { card: true }` adds the detail card under the status card. Anything else keeps the defaults.
12
+ */
13
+ export declare function readBandOptions(options: unknown): BandOptions;
14
+ /**
15
+ * The skill chips row, exactly `width` cells: bright accent pills for the current request first, then
16
+ * dim recent names, and `+N` for the ones that do not fit. The label goes before any chip does.
17
+ */
18
+ export declare function skillRow(chips: readonly SkillChip[], shimmer: ChipShimmer | undefined, palette: MissionPalette, width: number): Line;
19
+ export type CardFigures = {
20
+ readonly contextPercent: number | null;
21
+ readonly branch?: string;
22
+ readonly inLane: boolean;
23
+ };
24
+ export type CardInputs = {
25
+ readonly view: MissionView;
26
+ readonly frame: MotionFrame;
27
+ readonly chips: readonly SkillChip[];
28
+ readonly shimmer?: ChipShimmer;
29
+ readonly figures: CardFigures;
30
+ /** The project's last goal, shown on the track while the session has no lit mission. */
31
+ readonly last?: LastMission;
32
+ };
33
+ /**
34
+ * The status card: four rows of exactly `width` cells whether the session is idle or working, so
35
+ * the prompt never moves. Each row sheds its own low-priority parts as the terminal narrows.
36
+ */
37
+ export declare function statusCard(input: CardInputs, palette: MissionPalette, width: number): Line[];
38
+ export type BandCard = {
39
+ readonly title: string;
40
+ readonly border: string;
41
+ readonly rows: readonly Line[];
42
+ };
43
+ /**
44
+ * The opt-in detail card under the status card. It keeps a fixed four rows whenever a goal exists, warnings
45
+ * first, so turning a warning on or off never moves the prompt.
46
+ */
47
+ export declare function bandCard(view: MissionView, frame: MotionFrame, palette: MissionPalette, width: number): BandCard | undefined;