drexler 0.2.3 → 0.2.5
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 +9 -0
- package/package.json +1 -1
- package/src/commands.ts +131 -16
- package/src/ui/CommandPalette.tsx +32 -5
- package/src/ui/TranscriptViewport.tsx +56 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.5
|
|
4
|
+
|
|
5
|
+
- Made constrained slash commands open smoother option choosers, with `/theme` showing all theme choices as soon as the command is typed.
|
|
6
|
+
- Added richer theme descriptions and contextual hints in the command palette.
|
|
7
|
+
|
|
8
|
+
## 0.2.4
|
|
9
|
+
|
|
10
|
+
- Improved transcript readability with distinct user and Drexler turn blocks, role-specific accents, and clearer body markers.
|
|
11
|
+
|
|
3
12
|
## 0.2.3
|
|
4
13
|
|
|
5
14
|
- Restored full-terminal-width interactive chrome, including the chat input bar.
|
package/package.json
CHANGED
package/src/commands.ts
CHANGED
|
@@ -53,6 +53,7 @@ const WHITESPACE_RE = /\s+/;
|
|
|
53
53
|
export interface SlashCommand {
|
|
54
54
|
readonly name: string;
|
|
55
55
|
readonly description: string;
|
|
56
|
+
readonly hint?: string;
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
export const COMMAND_PALETTE: ReadonlyArray<SlashCommand> = [
|
|
@@ -76,49 +77,151 @@ export const COMMAND_PALETTE: ReadonlyArray<SlashCommand> = [
|
|
|
76
77
|
{ name: "/copy-last", description: "Copy last response" },
|
|
77
78
|
];
|
|
78
79
|
|
|
80
|
+
const THEME_PALETTE_COPY: Record<
|
|
81
|
+
ThemeName,
|
|
82
|
+
{ readonly description: string; readonly hint: string }
|
|
83
|
+
> = {
|
|
84
|
+
apollo: {
|
|
85
|
+
description: "Signature Drexler green",
|
|
86
|
+
hint: "default executive terminal",
|
|
87
|
+
},
|
|
88
|
+
amber: {
|
|
89
|
+
description: "Warm amber deal glow",
|
|
90
|
+
hint: "low-light command room",
|
|
91
|
+
},
|
|
92
|
+
mono: {
|
|
93
|
+
description: "Plain high-contrast text",
|
|
94
|
+
hint: "NO_COLOR friendly",
|
|
95
|
+
},
|
|
96
|
+
terminal: {
|
|
97
|
+
description: "Classic ANSI terminal",
|
|
98
|
+
hint: "green/cyan legacy mode",
|
|
99
|
+
},
|
|
100
|
+
dealroom: {
|
|
101
|
+
description: "Teal boardroom desk",
|
|
102
|
+
hint: "quiet professional palette",
|
|
103
|
+
},
|
|
104
|
+
midnight: {
|
|
105
|
+
description: "Cool blue night desk",
|
|
106
|
+
hint: "focused late-session work",
|
|
107
|
+
},
|
|
108
|
+
paper: {
|
|
109
|
+
description: "Clean document mode",
|
|
110
|
+
hint: "bright memo-style contrast",
|
|
111
|
+
},
|
|
112
|
+
plasma: {
|
|
113
|
+
description: "Magenta trading floor",
|
|
114
|
+
hint: "high-energy neon accent",
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
79
118
|
const ARGUMENT_PALETTE: ReadonlyArray<{
|
|
80
119
|
readonly command: string;
|
|
120
|
+
readonly baseDescription: string;
|
|
121
|
+
readonly baseHint: string;
|
|
81
122
|
readonly values: ReadonlyArray<SlashCommand>;
|
|
82
123
|
}> = [
|
|
83
124
|
{
|
|
84
125
|
command: "/theme",
|
|
126
|
+
baseDescription: "Theme chooser",
|
|
127
|
+
baseHint: "select a look below",
|
|
85
128
|
values: [
|
|
86
|
-
...THEME_NAMES.map((name) =>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
129
|
+
...THEME_NAMES.map((name) => {
|
|
130
|
+
const copy = THEME_PALETTE_COPY[name];
|
|
131
|
+
return {
|
|
132
|
+
name: `/theme ${name}`,
|
|
133
|
+
description: copy.description,
|
|
134
|
+
hint: copy.hint,
|
|
135
|
+
};
|
|
136
|
+
}),
|
|
137
|
+
{
|
|
138
|
+
name: "/theme save",
|
|
139
|
+
description: "Persist current theme",
|
|
140
|
+
hint: "use after previewing",
|
|
141
|
+
},
|
|
91
142
|
],
|
|
92
143
|
},
|
|
93
144
|
{
|
|
94
145
|
command: "/startup",
|
|
146
|
+
baseDescription: "Startup mode chooser",
|
|
147
|
+
baseHint: "pick launch behavior",
|
|
95
148
|
values: [
|
|
96
|
-
{
|
|
97
|
-
|
|
98
|
-
|
|
149
|
+
{
|
|
150
|
+
name: "/startup fast",
|
|
151
|
+
description: "Persist fast startup",
|
|
152
|
+
hint: "skip ceremony",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "/startup no-intro",
|
|
156
|
+
description: "Skip intro on launch",
|
|
157
|
+
hint: "keep normal runtime",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: "/startup normal",
|
|
161
|
+
description: "Restore full intro",
|
|
162
|
+
hint: "show full opening",
|
|
163
|
+
},
|
|
99
164
|
],
|
|
100
165
|
},
|
|
101
166
|
{
|
|
102
167
|
command: "/retry",
|
|
168
|
+
baseDescription: "Retry style chooser",
|
|
169
|
+
baseHint: "reshape last answer",
|
|
103
170
|
values: [
|
|
104
|
-
{
|
|
105
|
-
|
|
171
|
+
{
|
|
172
|
+
name: "/retry terse",
|
|
173
|
+
description: "Retry in two sentences",
|
|
174
|
+
hint: "short and direct",
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: "/retry brutal",
|
|
178
|
+
description: "Retry more forcefully",
|
|
179
|
+
hint: "sharper critique",
|
|
180
|
+
},
|
|
106
181
|
],
|
|
107
182
|
},
|
|
108
183
|
{
|
|
109
184
|
command: "/export",
|
|
185
|
+
baseDescription: "Export format chooser",
|
|
186
|
+
baseHint: "pick transcript output",
|
|
110
187
|
values: [
|
|
111
|
-
{
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
188
|
+
{
|
|
189
|
+
name: "/export md",
|
|
190
|
+
description: "Export markdown transcript",
|
|
191
|
+
hint: "portable notes",
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: "/export txt",
|
|
195
|
+
description: "Export plain text transcript",
|
|
196
|
+
hint: "clean copy",
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: "/export json",
|
|
200
|
+
description: "Export structured JSON",
|
|
201
|
+
hint: "machine-readable",
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: "/export html",
|
|
205
|
+
description: "Export printable HTML",
|
|
206
|
+
hint: "browser-ready memo",
|
|
207
|
+
},
|
|
115
208
|
],
|
|
116
209
|
},
|
|
117
210
|
{
|
|
118
211
|
command: "/model",
|
|
212
|
+
baseDescription: "Model chooser",
|
|
213
|
+
baseHint: "select inference desk",
|
|
119
214
|
values: [
|
|
120
|
-
{
|
|
121
|
-
|
|
215
|
+
{
|
|
216
|
+
name: "/model 31b",
|
|
217
|
+
description: "Use primary 31b model",
|
|
218
|
+
hint: "best default",
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: "/model 26b",
|
|
222
|
+
description: "Use fallback 26b model",
|
|
223
|
+
hint: "faster backup",
|
|
224
|
+
},
|
|
122
225
|
],
|
|
123
226
|
},
|
|
124
227
|
];
|
|
@@ -126,6 +229,16 @@ const ARGUMENT_PALETTE: ReadonlyArray<{
|
|
|
126
229
|
function filterArgumentPalette(input: string): ReadonlyArray<SlashCommand> {
|
|
127
230
|
const lower = input.toLowerCase();
|
|
128
231
|
for (const group of ARGUMENT_PALETTE) {
|
|
232
|
+
if (lower === group.command) {
|
|
233
|
+
return [
|
|
234
|
+
{
|
|
235
|
+
name: group.command,
|
|
236
|
+
description: group.baseDescription,
|
|
237
|
+
hint: group.baseHint,
|
|
238
|
+
},
|
|
239
|
+
...group.values,
|
|
240
|
+
];
|
|
241
|
+
}
|
|
129
242
|
const prefix = `${group.command} `;
|
|
130
243
|
if (!lower.startsWith(prefix)) continue;
|
|
131
244
|
const argPrefix = lower.slice(prefix.length);
|
|
@@ -143,6 +256,8 @@ export function filterPaletteByPrefix(
|
|
|
143
256
|
input: string,
|
|
144
257
|
): ReadonlyArray<SlashCommand> {
|
|
145
258
|
if (!input.startsWith("/")) return [];
|
|
259
|
+
const exactArgumentPalette = filterArgumentPalette(input);
|
|
260
|
+
if (exactArgumentPalette.length > 0) return exactArgumentPalette;
|
|
146
261
|
if (input.includes(" ")) return filterArgumentPalette(input);
|
|
147
262
|
const prefix = input.toLowerCase();
|
|
148
263
|
return COMMAND_PALETTE.filter((c) =>
|
|
@@ -31,10 +31,34 @@ const COMMAND_HINTS: Record<string, string> = {
|
|
|
31
31
|
"/copy-last": "copy latest response",
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
const ARGUMENT_TITLES: Record<string, { title: string; hint: string }> = {
|
|
35
|
+
"/theme": { title: "THEMES", hint: "tab fill, ↑↓ choose, enter apply" },
|
|
36
|
+
"/startup": { title: "STARTUP", hint: "tab fill, ↑↓ choose, enter save" },
|
|
37
|
+
"/retry": { title: "RETRY", hint: "tab fill, ↑↓ choose, enter reroll" },
|
|
38
|
+
"/export": { title: "EXPORT", hint: "tab fill, ↑↓ choose, enter run" },
|
|
39
|
+
"/model": { title: "MODELS", hint: "tab fill, ↑↓ choose, enter switch" },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function paletteHeading(items: ReadonlyArray<SlashCommand>): {
|
|
43
|
+
title: string;
|
|
44
|
+
hint: string;
|
|
45
|
+
} {
|
|
46
|
+
const firstToken = items[0]?.name.split(" ")[0] ?? "";
|
|
47
|
+
const hasArguments = items.some((item) => item.name.includes(" "));
|
|
48
|
+
if (hasArguments && ARGUMENT_TITLES[firstToken]) {
|
|
49
|
+
return ARGUMENT_TITLES[firstToken];
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
title: "DIRECTIVES",
|
|
53
|
+
hint: "tab/↑↓ select, enter execute",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
34
57
|
function CommandPaletteInner({ items, selectedIdx, width = 80 }: Props) {
|
|
35
58
|
const t = useTheme();
|
|
36
59
|
const safeWidth = Math.max(1, Math.floor(width));
|
|
37
60
|
const tiny = safeWidth < 26;
|
|
61
|
+
const heading = useMemo(() => paletteHeading(items), [items]);
|
|
38
62
|
const maxNameW = useMemo(
|
|
39
63
|
() => items.reduce((m, i) => Math.max(m, i.name.length), 0),
|
|
40
64
|
[items],
|
|
@@ -81,19 +105,22 @@ function CommandPaletteInner({ items, selectedIdx, width = 80 }: Props) {
|
|
|
81
105
|
>
|
|
82
106
|
<Box marginBottom={1}>
|
|
83
107
|
<Text color={t.primaryLight} bold>
|
|
84
|
-
|
|
108
|
+
{heading.title}
|
|
85
109
|
</Text>
|
|
86
110
|
<Text color={t.primaryDim}> ─ </Text>
|
|
87
111
|
<Text color={t.dim} wrap="truncate">
|
|
88
|
-
{fitDisplayText(
|
|
112
|
+
{fitDisplayText(
|
|
113
|
+
heading.hint,
|
|
114
|
+
Math.max(1, innerWidth - displayWidth(heading.title) - 3),
|
|
115
|
+
)}
|
|
89
116
|
</Text>
|
|
90
117
|
</Box>
|
|
91
118
|
{items.map((item, idx) => {
|
|
92
119
|
const sel = idx === selectedIdx;
|
|
93
120
|
const isArgumentSuggestion = item.name.includes(" ");
|
|
94
|
-
const hint =
|
|
95
|
-
|
|
96
|
-
: COMMAND_HINTS[item.name] ?? item.description;
|
|
121
|
+
const hint =
|
|
122
|
+
item.hint ??
|
|
123
|
+
(isArgumentSuggestion ? "" : COMMAND_HINTS[item.name] ?? item.description);
|
|
97
124
|
const name = item.name.padEnd(maxNameW + 1);
|
|
98
125
|
const desc = fitDisplayText(item.description, descBudget);
|
|
99
126
|
const clippedHint =
|
|
@@ -35,6 +35,18 @@ const ROLE_LABELS: Record<TranscriptViewportItem["role"], string> = {
|
|
|
35
35
|
system: "SYSTEM",
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
const ROLE_DETAILS: Record<TranscriptViewportItem["role"], string> = {
|
|
39
|
+
user: "incoming memo",
|
|
40
|
+
assistant: "response ledger",
|
|
41
|
+
system: "system notice",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const ROLE_MARKERS: Record<TranscriptViewportItem["role"], string> = {
|
|
45
|
+
user: "›",
|
|
46
|
+
assistant: "│",
|
|
47
|
+
system: "!",
|
|
48
|
+
};
|
|
49
|
+
|
|
38
50
|
function lineCount(input: string): number {
|
|
39
51
|
if (input.length === 0) return 1;
|
|
40
52
|
return input.split("\n").length;
|
|
@@ -42,17 +54,30 @@ function lineCount(input: string): number {
|
|
|
42
54
|
|
|
43
55
|
function itemRows(item: TranscriptViewportItem, compact: boolean): number {
|
|
44
56
|
if (compact) return 1;
|
|
45
|
-
return
|
|
57
|
+
return 2 + lineCount(item.content);
|
|
46
58
|
}
|
|
47
59
|
|
|
48
|
-
function
|
|
60
|
+
function roleAccentColor(
|
|
49
61
|
role: TranscriptViewportItem["role"],
|
|
50
62
|
theme: ReturnType<typeof useTheme>,
|
|
51
63
|
): string {
|
|
52
64
|
if (role === "system") return theme.warning;
|
|
65
|
+
if (role === "user") return theme.warning;
|
|
53
66
|
return theme.primaryLight;
|
|
54
67
|
}
|
|
55
68
|
|
|
69
|
+
function roleBodyColor(
|
|
70
|
+
role: TranscriptViewportItem["role"],
|
|
71
|
+
theme: ReturnType<typeof useTheme>,
|
|
72
|
+
): string {
|
|
73
|
+
if (role === "system") return theme.dim;
|
|
74
|
+
return theme.text;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function rule(char: string, width: number): string {
|
|
78
|
+
return char.repeat(Math.max(0, width));
|
|
79
|
+
}
|
|
80
|
+
|
|
56
81
|
function DefaultTranscriptItem({
|
|
57
82
|
item,
|
|
58
83
|
compact,
|
|
@@ -64,18 +89,20 @@ function DefaultTranscriptItem({
|
|
|
64
89
|
}) {
|
|
65
90
|
const t = useTheme();
|
|
66
91
|
const label = ROLE_LABELS[item.role];
|
|
92
|
+
const accent = roleAccentColor(item.role, t);
|
|
67
93
|
|
|
68
94
|
if (compact) {
|
|
69
|
-
const
|
|
95
|
+
const marker = item.role === "assistant" ? "◆" : ROLE_MARKERS[item.role];
|
|
96
|
+
const prefix = `${label} ${marker} `;
|
|
70
97
|
const budget = Math.max(1, cols - displayWidth(prefix));
|
|
71
98
|
const firstLine = item.content.split("\n")[0] ?? "";
|
|
72
99
|
return (
|
|
73
100
|
<Box width={cols} flexShrink={1}>
|
|
74
|
-
<Text color={
|
|
101
|
+
<Text color={accent} bold>
|
|
75
102
|
{fitDisplayText(prefix, cols)}
|
|
76
103
|
</Text>
|
|
77
104
|
{displayWidth(prefix) < cols ? (
|
|
78
|
-
<Text color={item.role
|
|
105
|
+
<Text color={roleBodyColor(item.role, t)} wrap="truncate">
|
|
79
106
|
{fitDisplayText(firstLine, budget)}
|
|
80
107
|
</Text>
|
|
81
108
|
) : null}
|
|
@@ -83,20 +110,38 @@ function DefaultTranscriptItem({
|
|
|
83
110
|
);
|
|
84
111
|
}
|
|
85
112
|
|
|
86
|
-
const
|
|
113
|
+
const detail = ROLE_DETAILS[item.role];
|
|
114
|
+
const headerPrefix = `╭─ ${label} `;
|
|
115
|
+
const headerSuffix = ` ${detail}`;
|
|
116
|
+
const headerRuleWidth = Math.max(
|
|
117
|
+
0,
|
|
118
|
+
cols - displayWidth(headerPrefix) - displayWidth(headerSuffix),
|
|
119
|
+
);
|
|
120
|
+
const footerWidth = Math.max(1, cols - 1);
|
|
121
|
+
const bodyPrefix = `${ROLE_MARKERS[item.role]} `;
|
|
122
|
+
const contentWidth = Math.max(1, cols - displayWidth(bodyPrefix));
|
|
123
|
+
|
|
87
124
|
return (
|
|
88
125
|
<Box flexDirection="column" width={cols} flexShrink={1}>
|
|
89
|
-
<Text color={
|
|
90
|
-
{fitDisplayText(
|
|
126
|
+
<Text color={accent} bold wrap="truncate">
|
|
127
|
+
{fitDisplayText(
|
|
128
|
+
`${headerPrefix}${rule("─", headerRuleWidth)}${headerSuffix}`,
|
|
129
|
+
cols,
|
|
130
|
+
)}
|
|
91
131
|
</Text>
|
|
92
132
|
{item.content.split("\n").map((line, index) => (
|
|
93
|
-
<Box key={index}
|
|
94
|
-
<Text color={
|
|
95
|
-
|
|
133
|
+
<Box key={index} width={cols} flexShrink={1}>
|
|
134
|
+
<Text color={accent} bold={item.role === "user"}>
|
|
135
|
+
{bodyPrefix}
|
|
136
|
+
</Text>
|
|
137
|
+
<Text color={roleBodyColor(item.role, t)} wrap="truncate">
|
|
96
138
|
{fitDisplayText(line, contentWidth)}
|
|
97
139
|
</Text>
|
|
98
140
|
</Box>
|
|
99
141
|
))}
|
|
142
|
+
<Text color={item.role === "assistant" ? t.primaryDim : t.dim} wrap="truncate">
|
|
143
|
+
{fitDisplayText(`╰${rule("─", footerWidth)}`, cols)}
|
|
144
|
+
</Text>
|
|
100
145
|
</Box>
|
|
101
146
|
);
|
|
102
147
|
}
|