@cueloop/client 0.1.0-alpha.58 → 0.1.0-alpha.59
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/package.json +5 -5
- package/src/App.tsx +174 -574
- package/src/app-key-state.ts +60 -0
- package/src/app-screens.tsx +266 -0
- package/src/app-view-model.ts +286 -0
- package/src/components/PlanSheet.tsx +2 -62
- package/src/components/plan-sheet-run-style.ts +86 -0
- package/src/config.ts +61 -47
- package/src/intent-dispatch.ts +427 -307
- package/src/keymap.ts +81 -59
- package/src/use-settings-dialog.tsx +263 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createTextAttributes } from "@opentui/core";
|
|
2
|
+
import type { DisplayBlock, StyleRun } from "../view-plan";
|
|
3
|
+
import type { Theme } from "../theme";
|
|
4
|
+
|
|
5
|
+
/** A cut block reads as removed: struck through and grayed, never red. */
|
|
6
|
+
const CUT_ATTRIBUTES = createTextAttributes({ strikethrough: true, dim: true });
|
|
7
|
+
|
|
8
|
+
/** Inline-emphasis attributes, reused per run so the render loop allocates none. */
|
|
9
|
+
const STRONG_ATTRIBUTES = createTextAttributes({ bold: true });
|
|
10
|
+
const EM_ATTRIBUTES = createTextAttributes({ italic: true });
|
|
11
|
+
const STRIKE_ATTRIBUTES = createTextAttributes({ strikethrough: true });
|
|
12
|
+
const LINK_ATTRIBUTES = createTextAttributes({ underline: true });
|
|
13
|
+
/** Block-level base attributes: headings read bold, blockquotes read italic. */
|
|
14
|
+
const HEADING_ATTRIBUTES = createTextAttributes({ bold: true });
|
|
15
|
+
const QUOTE_ATTRIBUTES = createTextAttributes({ italic: true });
|
|
16
|
+
|
|
17
|
+
interface BlockBaseStyle {
|
|
18
|
+
baseFg: string;
|
|
19
|
+
baseAttributes: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isHeadingBlock(block: DisplayBlock): boolean {
|
|
23
|
+
return block.kind === "h1" || block.kind === "h2" || block.kind === "h3";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// headings are all bold; level reads from descending brightness alone (a
|
|
27
|
+
// terminal cannot scale font size), leaving the salmon accent to annotations
|
|
28
|
+
function headingForeground(block: DisplayBlock, tokens: Theme): string | undefined {
|
|
29
|
+
if (block.kind === "h1") return tokens.text;
|
|
30
|
+
if (block.kind === "h2") return tokens.textMuted;
|
|
31
|
+
if (block.kind === "h3") return tokens.textDim;
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function blockBaseStyle(block: DisplayBlock, tokens: Theme): BlockBaseStyle {
|
|
36
|
+
const isHeading = isHeadingBlock(block);
|
|
37
|
+
const isQuote = block.kind === "quote";
|
|
38
|
+
const headingFg = headingForeground(block, tokens);
|
|
39
|
+
// block-level base: headings bold, quotes muted italic; inline roles compose on top
|
|
40
|
+
const baseFg = headingFg ?? (isQuote ? tokens.textMuted : tokens.text);
|
|
41
|
+
const baseAttributes = (isHeading ? HEADING_ATTRIBUTES : 0) | (isQuote ? QUOTE_ATTRIBUTES : 0);
|
|
42
|
+
return { baseFg, baseAttributes };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function inlineRoleStyle(
|
|
46
|
+
run: StyleRun,
|
|
47
|
+
base: BlockBaseStyle,
|
|
48
|
+
tokens: Theme,
|
|
49
|
+
): { fg?: string; bg?: string; attributes?: number } {
|
|
50
|
+
const { baseFg, baseAttributes } = base;
|
|
51
|
+
const withBase = (roleAttributes: number): number => baseAttributes | roleAttributes;
|
|
52
|
+
switch (run.role) {
|
|
53
|
+
case "ins":
|
|
54
|
+
return { fg: tokens.insertedForeground, attributes: baseAttributes || undefined };
|
|
55
|
+
case "del":
|
|
56
|
+
return { fg: tokens.deletedForeground };
|
|
57
|
+
case "mark-comment":
|
|
58
|
+
return { fg: tokens.text, bg: tokens.markCommentBackground };
|
|
59
|
+
case "mark-focus":
|
|
60
|
+
return { fg: tokens.accentInk, bg: tokens.accent };
|
|
61
|
+
case "kspan":
|
|
62
|
+
return { fg: tokens.accentInk, bg: tokens.accent };
|
|
63
|
+
case "strong":
|
|
64
|
+
return { fg: baseFg, attributes: withBase(STRONG_ATTRIBUTES) };
|
|
65
|
+
case "em":
|
|
66
|
+
return { fg: baseFg, attributes: withBase(EM_ATTRIBUTES) };
|
|
67
|
+
case "code":
|
|
68
|
+
return { fg: tokens.text, bg: tokens.elevated };
|
|
69
|
+
case "strike":
|
|
70
|
+
return { fg: tokens.textMuted, attributes: withBase(STRIKE_ATTRIBUTES) };
|
|
71
|
+
case "link":
|
|
72
|
+
return { fg: tokens.blue, attributes: withBase(LINK_ATTRIBUTES) };
|
|
73
|
+
default:
|
|
74
|
+
return { fg: baseFg, attributes: baseAttributes || undefined };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function runStyle(
|
|
79
|
+
run: StyleRun,
|
|
80
|
+
block: DisplayBlock,
|
|
81
|
+
tokens: Theme,
|
|
82
|
+
): { fg?: string; bg?: string; attributes?: number } {
|
|
83
|
+
// a cut block reads as removed: every run struck through and grayed, never red
|
|
84
|
+
if (block.type === "del") return { fg: tokens.textDim, attributes: CUT_ATTRIBUTES };
|
|
85
|
+
return inlineRoleStyle(run, blockBaseStyle(block, tokens), tokens);
|
|
86
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -160,6 +160,63 @@ function parseActions(raw: unknown): QuickAction[] | undefined {
|
|
|
160
160
|
return actions.length ? actions : undefined;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
function mergeAuthors(target: Record<string, string>, raw: unknown): void {
|
|
164
|
+
const authors = raw as Record<string, unknown> | undefined;
|
|
165
|
+
if (!authors) return;
|
|
166
|
+
for (const [id, value] of Object.entries(authors)) {
|
|
167
|
+
if (typeof value === "string") target[id] = value;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function mergeKeys(target: Record<string, string[]>, raw: unknown): void {
|
|
172
|
+
const keys = raw as KeymapConfig | undefined;
|
|
173
|
+
if (!keys) return;
|
|
174
|
+
for (const [action, combo] of Object.entries(keys)) {
|
|
175
|
+
target[action] = Array.isArray(combo) ? combo : [combo];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function mergeAutoClose(target: CueloopConfig["ui"], value: unknown): void {
|
|
180
|
+
if (value === undefined) return;
|
|
181
|
+
if (value === "off") target.autoClose = "off";
|
|
182
|
+
else if (typeof value === "number" && value >= 0) target.autoClose = value;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function mergeUi(target: CueloopConfig["ui"], raw: unknown): void {
|
|
186
|
+
const ui = raw as
|
|
187
|
+
| { auto_close?: unknown; editor?: unknown; review_width?: unknown; review_state?: unknown }
|
|
188
|
+
| undefined;
|
|
189
|
+
if (!ui) return;
|
|
190
|
+
mergeAutoClose(target, ui.auto_close);
|
|
191
|
+
if (typeof ui.editor === "string" && ui.editor.trim()) target.editor = ui.editor.trim();
|
|
192
|
+
if (typeof ui.review_width === "number" && Number.isFinite(ui.review_width)) {
|
|
193
|
+
target.reviewWidth = clampWidth(ui.review_width);
|
|
194
|
+
}
|
|
195
|
+
if (
|
|
196
|
+
ui.review_state === "expanded" ||
|
|
197
|
+
ui.review_state === "compact" ||
|
|
198
|
+
ui.review_state === "hidden"
|
|
199
|
+
) {
|
|
200
|
+
target.reviewState = ui.review_state;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function mergeObsidian(target: ObsidianConfig, raw: unknown): void {
|
|
205
|
+
const integrations = raw as Record<string, unknown> | undefined;
|
|
206
|
+
const obsidian = integrations?.["obsidian"] as Record<string, unknown> | undefined;
|
|
207
|
+
if (!obsidian) return;
|
|
208
|
+
if (typeof obsidian["vault"] === "string") target.vault = obsidian["vault"];
|
|
209
|
+
if (typeof obsidian["folder"] === "string") target.folder = obsidian["folder"];
|
|
210
|
+
if (typeof obsidian["filenameFormat"] === "string")
|
|
211
|
+
target.filenameFormat = obsidian["filenameFormat"];
|
|
212
|
+
const separator = obsidian["separator"];
|
|
213
|
+
if (separator === "space" || separator === "dash" || separator === "underscore")
|
|
214
|
+
target.separator = separator;
|
|
215
|
+
const exportOn = obsidian["exportOn"];
|
|
216
|
+
if (exportOn === "approve" || exportOn === "resolve" || exportOn === "manual")
|
|
217
|
+
target.exportOn = exportOn;
|
|
218
|
+
}
|
|
219
|
+
|
|
163
220
|
function layer(base: CueloopConfig, raw: Record<string, unknown>): CueloopConfig {
|
|
164
221
|
const out: CueloopConfig = {
|
|
165
222
|
keys: { ...base.keys },
|
|
@@ -172,53 +229,10 @@ function layer(base: CueloopConfig, raw: Record<string, unknown>): CueloopConfig
|
|
|
172
229
|
};
|
|
173
230
|
const actions = parseActions(raw["actions"]);
|
|
174
231
|
if (actions) out.actions = actions;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
const keys = raw["keys"] as KeymapConfig | undefined;
|
|
182
|
-
if (keys) {
|
|
183
|
-
for (const [action, combo] of Object.entries(keys)) {
|
|
184
|
-
out.keys[action] = Array.isArray(combo) ? combo : [combo];
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
const ui = raw["ui"] as
|
|
188
|
-
| { auto_close?: unknown; editor?: unknown; review_width?: unknown; review_state?: unknown }
|
|
189
|
-
| undefined;
|
|
190
|
-
if (ui && ui.auto_close !== undefined) {
|
|
191
|
-
if (ui.auto_close === "off") out.ui.autoClose = "off";
|
|
192
|
-
else if (typeof ui.auto_close === "number" && ui.auto_close >= 0)
|
|
193
|
-
out.ui.autoClose = ui.auto_close;
|
|
194
|
-
}
|
|
195
|
-
if (ui && typeof ui.editor === "string" && ui.editor.trim()) out.ui.editor = ui.editor.trim();
|
|
196
|
-
if (ui && typeof ui.review_width === "number" && Number.isFinite(ui.review_width)) {
|
|
197
|
-
out.ui.reviewWidth = clampWidth(ui.review_width);
|
|
198
|
-
}
|
|
199
|
-
if (
|
|
200
|
-
ui &&
|
|
201
|
-
(ui.review_state === "expanded" ||
|
|
202
|
-
ui.review_state === "compact" ||
|
|
203
|
-
ui.review_state === "hidden")
|
|
204
|
-
) {
|
|
205
|
-
out.ui.reviewState = ui.review_state;
|
|
206
|
-
}
|
|
207
|
-
const integrations = raw["integrations"] as Record<string, unknown> | undefined;
|
|
208
|
-
const obsidian = integrations?.["obsidian"] as Record<string, unknown> | undefined;
|
|
209
|
-
if (obsidian) {
|
|
210
|
-
const obsidianConfig = out.integrations.obsidian;
|
|
211
|
-
if (typeof obsidian["vault"] === "string") obsidianConfig.vault = obsidian["vault"];
|
|
212
|
-
if (typeof obsidian["folder"] === "string") obsidianConfig.folder = obsidian["folder"];
|
|
213
|
-
if (typeof obsidian["filenameFormat"] === "string")
|
|
214
|
-
obsidianConfig.filenameFormat = obsidian["filenameFormat"];
|
|
215
|
-
const separator = obsidian["separator"];
|
|
216
|
-
if (separator === "space" || separator === "dash" || separator === "underscore")
|
|
217
|
-
obsidianConfig.separator = separator;
|
|
218
|
-
const exportOn = obsidian["exportOn"];
|
|
219
|
-
if (exportOn === "approve" || exportOn === "resolve" || exportOn === "manual")
|
|
220
|
-
obsidianConfig.exportOn = exportOn;
|
|
221
|
-
}
|
|
232
|
+
mergeAuthors(out.authors, raw["authors"]);
|
|
233
|
+
mergeKeys(out.keys, raw["keys"]);
|
|
234
|
+
mergeUi(out.ui, raw["ui"]);
|
|
235
|
+
mergeObsidian(out.integrations.obsidian, raw["integrations"]);
|
|
222
236
|
return out;
|
|
223
237
|
}
|
|
224
238
|
|