drexler 0.2.12 → 0.2.14
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 +15 -0
- package/README.md +58 -13
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/ui/App.tsx +26 -14
- package/src/ui/CommandPalette.tsx +23 -13
- package/src/ui/DealDeskHeader.tsx +248 -80
- package/src/ui/MarkdownBody.tsx +382 -0
- package/src/ui/MascotIntro.tsx +568 -73
- package/src/ui/Message.tsx +28 -15
- package/src/ui/Spinner.tsx +11 -9
- package/src/ui/StatusBar.tsx +11 -43
- package/src/ui/SynergyEvent.tsx +3 -2
- package/src/ui/TranscriptViewport.tsx +271 -30
- package/src/ui/displayContent.ts +114 -0
- package/src/ui/graphemes.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.14
|
|
4
|
+
|
|
5
|
+
- Added a startup Mood panel with a stable boot gauge, percentage-only loading row, and rotating mood-specific posture/detail copy.
|
|
6
|
+
- Anchored the wide startup dashboard so wrapped greeting copy no longer pushes the Mood and Deal Desk boxes down or adds stray rows.
|
|
7
|
+
- Reworked the embedded Deal Desk into satirical mood-shaped product chrome instead of model/context telemetry.
|
|
8
|
+
- Improved Drexler transcript rendering with complete bordered cards, a diamond response marker, cleaned markdown/code fence display, and Dracula-inspired code syntax colors.
|
|
9
|
+
- Updated documentation to match current startup chrome, command palette behavior, keyboard controls, source setup, and layout invariants.
|
|
10
|
+
|
|
11
|
+
## 0.2.13
|
|
12
|
+
|
|
13
|
+
- Hardened startup panel layout across narrow, standard, and wide terminals.
|
|
14
|
+
- Clamped the embedded Deal Desk to its actual startup-panel column.
|
|
15
|
+
- Improved display-width clipping for Deal Desk, command palette, spinner, status bar, and transcript row budgeting.
|
|
16
|
+
- Added regression coverage for duplicate startup chrome, wide glyphs, long command rows, and short-terminal startup suppression.
|
|
17
|
+
|
|
3
18
|
## 0.2.12
|
|
4
19
|
|
|
5
20
|
- Removed the duplicate startup card render so normal launches show one startup panel.
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](./LICENSE)
|
|
5
5
|
[](https://bun.sh)
|
|
6
6
|
|
|
7
|
-
CLI chat with **Drexler**, a corporate-executive AI persona who speaks in broken third-person and treats every conversation like a hostile takeover. Built with Bun + TypeScript
|
|
7
|
+
CLI chat with **Drexler**, a corporate-executive AI persona who speaks in broken third-person and treats every conversation like a hostile takeover. Built with Bun + TypeScript, Ink, and OpenRouter-compatible Gemma models.
|
|
8
8
|
|
|
9
9
|
> "Drexler usually charge consulting fee for this. Today, pro bono. You welcome."
|
|
10
10
|
|
|
@@ -13,7 +13,7 @@ CLI chat with **Drexler**, a corporate-executive AI persona who speaks in broken
|
|
|
13
13
|
## Quickstart
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
bun
|
|
16
|
+
bun install -g drexler@latest
|
|
17
17
|
drexler
|
|
18
18
|
```
|
|
19
19
|
|
|
@@ -39,7 +39,7 @@ Verify: `bun --version` → should print `1.1.0` or higher.
|
|
|
39
39
|
### 2. Install Drexler globally
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
bun
|
|
42
|
+
bun install -g drexler@latest
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
This installs the `drexler` command into `~/.bun/bin/drexler`. Make sure `~/.bun/bin` is on your `$PATH` (Bun's installer does this automatically; if not, add `export PATH="$HOME/.bun/bin:$PATH"` to your shell rc).
|
|
@@ -65,9 +65,11 @@ Paste the key, hit return. Drexler saves it to `~/.config/drexler/config.json` (
|
|
|
65
65
|
## Update
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
|
-
bun
|
|
68
|
+
bun install -g drexler@latest
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
+
Global installs replace the existing `drexler` package in Bun's global install location; they do not keep stacking duplicate app copies.
|
|
72
|
+
|
|
71
73
|
## Uninstall
|
|
72
74
|
|
|
73
75
|
```bash
|
|
@@ -79,6 +81,33 @@ rm -rf ~/.config/drexler # optional: wipe stored key + settings
|
|
|
79
81
|
|
|
80
82
|
## Usage
|
|
81
83
|
|
|
84
|
+
### Interactive UI
|
|
85
|
+
|
|
86
|
+
Drexler runs as an Ink terminal UI when both stdin and stdout are TTYs. The normal launch shows one integrated startup panel with the mascot, tips, a **Mood** readout, and the **Drexler Deal Desk**. Short terminals automatically suppress oversized startup chrome so the chat stays usable.
|
|
87
|
+
|
|
88
|
+
The startup panel is designed to stay stable while it boots: the mascot loading bar and Mood gauge animate without changing width, greeting copy is held in a fixed slot, and the Mood and Deal Desk boxes stay aligned when the greeting wraps. After boot, Mood resolves into a rotating Drexler-flavored posture with a short satirical subtext line.
|
|
89
|
+
|
|
90
|
+
The Deal Desk is intentionally not a frontier-model telemetry panel. It shows mood-shaped corporate nonsense like boardroom status, memo count, mandate, risk, fees, and counsel posture. Values rotate by mood and session so repeated moods still feel alive.
|
|
91
|
+
|
|
92
|
+
Conversation turns render as bordered cards aligned to the chat input width. User and Drexler responses use separate accents, wrapped text, and fixed-width borders so long responses stay inside the terminal instead of clipping at the right edge. Drexler responses use a diamond body marker. Markdown/code fence labels are cleaned up for display, and code blocks render with Dracula-inspired terminal syntax colors.
|
|
93
|
+
|
|
94
|
+
Typing `/` opens the directive palette. Use `Tab`, `Enter`, or `↑`/`↓` to select. Commands with fixed arguments open smoother option choosers:
|
|
95
|
+
|
|
96
|
+
- `/theme` previews all themes with descriptions.
|
|
97
|
+
- `/startup` offers `fast`, `no-intro`, and `normal`.
|
|
98
|
+
- `/retry` offers `terse` and `brutal`.
|
|
99
|
+
- `/export` offers `md`, `txt`, `json`, and `html`.
|
|
100
|
+
- `/model` offers `31b` and `26b`.
|
|
101
|
+
|
|
102
|
+
`/synergy` runs a rotating animated corporate event in the live UI, then returns control to the chat when the animation completes.
|
|
103
|
+
|
|
104
|
+
Keyboard notes:
|
|
105
|
+
|
|
106
|
+
- `Tab`, `Enter`, and `↑`/`↓` operate the directive palette.
|
|
107
|
+
- `PageUp`/`PageDown` scroll transcript history when it exceeds the visible viewport.
|
|
108
|
+
- `Esc` cancels an in-flight model response without quitting.
|
|
109
|
+
- `Ctrl+C` exits gracefully with an in-character farewell.
|
|
110
|
+
|
|
82
111
|
### Flags
|
|
83
112
|
|
|
84
113
|
| flag | what |
|
|
@@ -93,12 +122,12 @@ rm -rf ~/.config/drexler # optional: wipe stored key + settings
|
|
|
93
122
|
|
|
94
123
|
### Slash commands (inside the REPL)
|
|
95
124
|
|
|
96
|
-
| cmd | what it
|
|
125
|
+
| cmd | what it does |
|
|
97
126
|
| -------------- | ------------------------------------------------ |
|
|
98
127
|
| `/help` | list directives |
|
|
99
128
|
| `/clear` | shred conversation history (system prompt pinned) |
|
|
100
129
|
| `/exit` | meeting adjourned |
|
|
101
|
-
| `/synergy` |
|
|
130
|
+
| `/synergy` | run a rotating animated morale event |
|
|
102
131
|
| `/model` | show current model, or `/model 26b` to switch |
|
|
103
132
|
| `/theme` | show/switch theme; append `save` to persist, e.g. `/theme midnight save` |
|
|
104
133
|
| `/startup fast\|no-intro\|normal` | persist startup behavior for future launches |
|
|
@@ -114,8 +143,6 @@ rm -rf ~/.config/drexler # optional: wipe stored key + settings
|
|
|
114
143
|
| `/save-last [path]` | save Drexler's last response only |
|
|
115
144
|
| `/copy-last` | copy Drexler's latest response to the clipboard |
|
|
116
145
|
|
|
117
|
-
`Ctrl+C` exits gracefully with an in-character farewell.
|
|
118
|
-
|
|
119
146
|
---
|
|
120
147
|
|
|
121
148
|
## Configuration
|
|
@@ -126,6 +153,8 @@ Drexler reads config in this priority (later wins):
|
|
|
126
153
|
2. Environment variables
|
|
127
154
|
3. CLI flags
|
|
128
155
|
|
|
156
|
+
If the current config file does not exist, Drexler also checks the legacy `~/.drexlerrc` path.
|
|
157
|
+
|
|
129
158
|
### Environment variables
|
|
130
159
|
|
|
131
160
|
| var | purpose |
|
|
@@ -159,13 +188,26 @@ Default `maxHistory`: 50 messages.
|
|
|
159
188
|
Available launch/config themes: `apollo`, `amber`, `mono`, `terminal`, `dealroom`, `midnight`, `paper`, and `plasma`.
|
|
160
189
|
`NO_COLOR` always forces `mono`.
|
|
161
190
|
|
|
191
|
+
Theme notes:
|
|
192
|
+
|
|
193
|
+
| theme | character |
|
|
194
|
+
| ---------- | ------------------------------------------ |
|
|
195
|
+
| `apollo` | signature Drexler green, the default |
|
|
196
|
+
| `amber` | warm amber deal glow |
|
|
197
|
+
| `mono` | plain high-contrast ANSI colors |
|
|
198
|
+
| `terminal` | classic green/cyan terminal |
|
|
199
|
+
| `dealroom` | restrained teal boardroom palette |
|
|
200
|
+
| `midnight` | cool blue late-session desk |
|
|
201
|
+
| `paper` | clean document-style contrast |
|
|
202
|
+
| `plasma` | high-energy magenta trading-floor accent |
|
|
203
|
+
|
|
162
204
|
---
|
|
163
205
|
|
|
164
206
|
## Models
|
|
165
207
|
|
|
166
208
|
| alias | id | notes |
|
|
167
209
|
| ------ | ----------------------------------- | ------------------------------ |
|
|
168
|
-
| `31b` | `google/gemma-4-31b-it` | primary
|
|
210
|
+
| `31b` | `google/gemma-4-31b-it` | primary default |
|
|
169
211
|
| `26b` | `google/gemma-4-26b-a4b-it` | fallback, auto-retry on 429 |
|
|
170
212
|
|
|
171
213
|
Pass `--model vendor/name:tag` for any other OpenRouter-compatible model.
|
|
@@ -178,7 +220,7 @@ Pass `--model vendor/name:tag` for any other OpenRouter-compatible model.
|
|
|
178
220
|
git clone https://github.com/showOS/Drexler.git
|
|
179
221
|
cd Drexler
|
|
180
222
|
bun install
|
|
181
|
-
|
|
223
|
+
export OPENROUTER_API_KEY=sk-or-v1-your-key
|
|
182
224
|
bun run start
|
|
183
225
|
```
|
|
184
226
|
|
|
@@ -192,11 +234,12 @@ bun run typecheck
|
|
|
192
234
|
### Releasing a new version
|
|
193
235
|
|
|
194
236
|
```bash
|
|
195
|
-
|
|
196
|
-
|
|
237
|
+
bun run prepublishOnly
|
|
238
|
+
npm version <patch|minor> # bumps package.json, commits, and tags
|
|
239
|
+
git push origin main --follow-tags
|
|
197
240
|
```
|
|
198
241
|
|
|
199
|
-
The `.github/workflows/publish.yml` workflow runs typecheck
|
|
242
|
+
The `.github/workflows/publish.yml` workflow runs install, tag/package version verification, typecheck, tests, and `npm publish --provenance` on every `v*` tag push.
|
|
200
243
|
|
|
201
244
|
---
|
|
202
245
|
|
|
@@ -210,6 +253,8 @@ The `.github/workflows/publish.yml` workflow runs typecheck + tests + `npm publi
|
|
|
210
253
|
| Garbled box-drawing characters | Use a UTF-8 terminal with a Nerd Font (e.g. iTerm2, Alacritty, WezTerm) |
|
|
211
254
|
| Want to switch themes mid-session | Use `/theme midnight`, `/theme dealroom`, `/theme amber`, or any listed theme inside the REPL |
|
|
212
255
|
| Want a faster launch | Use `drexler --fast` or set `"fast": true` in config |
|
|
256
|
+
| Startup panel looks cramped | Enlarge the terminal, or use `/startup no-intro` or `/startup fast` |
|
|
257
|
+
| Slash command options are not visible | Type `/`, `/theme`, `/startup`, `/retry`, `/export`, or `/model`; exact fixed-argument commands open their chooser automatically |
|
|
213
258
|
|
|
214
259
|
---
|
|
215
260
|
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
package/src/ui/App.tsx
CHANGED
|
@@ -34,7 +34,11 @@ import {
|
|
|
34
34
|
insertAtCursor,
|
|
35
35
|
} from "./graphemes.ts";
|
|
36
36
|
import { InputBox } from "./InputBox.tsx";
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
introPhaseColor,
|
|
39
|
+
MascotDashboard,
|
|
40
|
+
useIntroAnimation,
|
|
41
|
+
} from "./MascotIntro.tsx";
|
|
38
42
|
import { StreamingMessage } from "./Message.tsx";
|
|
39
43
|
import { Spinner } from "./Spinner.tsx";
|
|
40
44
|
import { StatusBar } from "./StatusBar.tsx";
|
|
@@ -136,14 +140,17 @@ export function App({
|
|
|
136
140
|
const chromeWidth = useMemo(() => Math.max(1, cols), [cols]);
|
|
137
141
|
const statusBarWidth = inputWidth;
|
|
138
142
|
const isCompact = mode === "very-narrow";
|
|
139
|
-
const integratedIntro =
|
|
143
|
+
const integratedIntro =
|
|
144
|
+
showIntroChrome && typeof greeting === "string" && rows >= 32;
|
|
145
|
+
const introRowBudget =
|
|
146
|
+
integratedIntro ? (chromeWidth >= 112 ? 14 : chromeWidth >= 72 ? 26 : 6) : 0;
|
|
140
147
|
const maxTranscriptRows = useMemo(
|
|
141
148
|
() =>
|
|
142
149
|
Math.max(
|
|
143
150
|
1,
|
|
144
|
-
transcriptRowsForTerminalRows(rows) -
|
|
151
|
+
transcriptRowsForTerminalRows(rows) - introRowBudget,
|
|
145
152
|
),
|
|
146
|
-
[
|
|
153
|
+
[introRowBudget, rows],
|
|
147
154
|
);
|
|
148
155
|
|
|
149
156
|
const [items, setItems] = useState<ChatItem[]>([]);
|
|
@@ -201,6 +208,7 @@ export function App({
|
|
|
201
208
|
const [historyIdx, setHistoryIdx] = useState<number | null>(null);
|
|
202
209
|
const [paletteIdx, setPaletteIdx] = useState(0);
|
|
203
210
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
211
|
+
const intro = useIntroAnimation(chromeWidth, integratedIntro);
|
|
204
212
|
|
|
205
213
|
const paletteItems = useMemo(() => filterPaletteByPrefix(input), [input]);
|
|
206
214
|
const paletteOpen = paletteItems.length > 0;
|
|
@@ -716,11 +724,7 @@ export function App({
|
|
|
716
724
|
const isBusy =
|
|
717
725
|
requestInFlight || streaming !== null || thinking !== null || synergyEvent !== null;
|
|
718
726
|
const headerStatus = isBusy ? "streaming" : deskStatus;
|
|
719
|
-
const
|
|
720
|
-
chromeWidth >= 112
|
|
721
|
-
? Math.min(72, Math.max(42, Math.floor(chromeWidth * 0.34)))
|
|
722
|
-
: Math.max(32, chromeWidth - 8);
|
|
723
|
-
const dealDeskHeader = (
|
|
727
|
+
const renderDealDeskHeader = (width: number) => (
|
|
724
728
|
<DealDeskHeader
|
|
725
729
|
model={model}
|
|
726
730
|
mood={mood}
|
|
@@ -732,13 +736,15 @@ export function App({
|
|
|
732
736
|
status={headerStatus}
|
|
733
737
|
compact={isCompact}
|
|
734
738
|
notice={!integratedIntro ? deskNotice ?? undefined : undefined}
|
|
735
|
-
maxWidth={
|
|
739
|
+
maxWidth={Math.max(1, width)}
|
|
736
740
|
marginBottom={integratedIntro ? 0 : 1}
|
|
737
741
|
/>
|
|
738
742
|
);
|
|
743
|
+
const dealDeskHeader = renderDealDeskHeader(chromeWidth);
|
|
739
744
|
const visibleTranscriptRows = synergyEvent
|
|
740
745
|
? Math.max(1, maxTranscriptRows - synergyEventRows(chromeWidth, isCompact))
|
|
741
746
|
: maxTranscriptRows;
|
|
747
|
+
const introBarColor = introPhaseColor(intro.colorPhase, t);
|
|
742
748
|
|
|
743
749
|
return (
|
|
744
750
|
<ThemeProvider value={activeTheme}>
|
|
@@ -748,7 +754,13 @@ export function App({
|
|
|
748
754
|
<MascotDashboard
|
|
749
755
|
greeting={greeting}
|
|
750
756
|
width={chromeWidth}
|
|
751
|
-
|
|
757
|
+
mood={mood}
|
|
758
|
+
bootProgress={intro.progress}
|
|
759
|
+
state={intro.state}
|
|
760
|
+
bar={intro.bar}
|
|
761
|
+
barColor={introBarColor}
|
|
762
|
+
mascotStatus={intro.status}
|
|
763
|
+
dealDesk={renderDealDeskHeader}
|
|
752
764
|
/>
|
|
753
765
|
</Box>
|
|
754
766
|
) : (
|
|
@@ -769,7 +781,7 @@ export function App({
|
|
|
769
781
|
</Box>
|
|
770
782
|
)}
|
|
771
783
|
{thinking !== null && streaming === null && (
|
|
772
|
-
<Box
|
|
784
|
+
<Box marginBottom={1}>
|
|
773
785
|
<Spinner label={thinking} width={chromeWidth} />
|
|
774
786
|
</Box>
|
|
775
787
|
)}
|
|
@@ -809,11 +821,11 @@ export function App({
|
|
|
809
821
|
width={inputWidth}
|
|
810
822
|
/>
|
|
811
823
|
</Box>
|
|
812
|
-
<Box>
|
|
824
|
+
<Box paddingLeft={2}>
|
|
813
825
|
<StatusBar
|
|
814
826
|
messageCount={msgCount}
|
|
815
827
|
witticism={witticism}
|
|
816
|
-
maxWidth={statusBarWidth}
|
|
828
|
+
maxWidth={Math.max(1, statusBarWidth - 2)}
|
|
817
829
|
status={isBusy ? "streaming" : deskStatus}
|
|
818
830
|
compact={isCompact}
|
|
819
831
|
scrollHint={scrollHint}
|
|
@@ -54,13 +54,18 @@ function paletteHeading(items: ReadonlyArray<SlashCommand>): {
|
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
function padDisplayText(input: string, width: number): string {
|
|
58
|
+
const clipped = fitDisplayText(input, width);
|
|
59
|
+
return `${clipped}${" ".repeat(Math.max(0, width - displayWidth(clipped)))}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
57
62
|
function CommandPaletteInner({ items, selectedIdx, width = 80 }: Props) {
|
|
58
63
|
const t = useTheme();
|
|
59
64
|
const safeWidth = Math.max(1, Math.floor(width));
|
|
60
65
|
const tiny = safeWidth < 26;
|
|
61
66
|
const heading = useMemo(() => paletteHeading(items), [items]);
|
|
62
67
|
const maxNameW = useMemo(
|
|
63
|
-
() => items.reduce((m, i) => Math.max(m, i.name
|
|
68
|
+
() => items.reduce((m, i) => Math.max(m, displayWidth(i.name)), 0),
|
|
64
69
|
[items],
|
|
65
70
|
);
|
|
66
71
|
if (items.length === 0) return null;
|
|
@@ -87,10 +92,21 @@ function CommandPaletteInner({ items, selectedIdx, width = 80 }: Props) {
|
|
|
87
92
|
}
|
|
88
93
|
|
|
89
94
|
const innerWidth = Math.max(1, safeWidth - 4);
|
|
90
|
-
const
|
|
95
|
+
const markerWidth = 2;
|
|
96
|
+
const nameBudget = Math.max(
|
|
97
|
+
6,
|
|
98
|
+
Math.min(maxNameW + 1, Math.floor(innerWidth * 0.44)),
|
|
99
|
+
);
|
|
100
|
+
const descBudget = Math.max(
|
|
101
|
+
6,
|
|
102
|
+
Math.min(
|
|
103
|
+
Math.floor(innerWidth * 0.34),
|
|
104
|
+
innerWidth - markerWidth - nameBudget - 1,
|
|
105
|
+
),
|
|
106
|
+
);
|
|
91
107
|
const hintBudget = Math.max(
|
|
92
108
|
0,
|
|
93
|
-
innerWidth -
|
|
109
|
+
innerWidth - markerWidth - nameBudget - 1 - descBudget - 2,
|
|
94
110
|
);
|
|
95
111
|
|
|
96
112
|
return (
|
|
@@ -121,18 +137,12 @@ function CommandPaletteInner({ items, selectedIdx, width = 80 }: Props) {
|
|
|
121
137
|
const hint =
|
|
122
138
|
item.hint ??
|
|
123
139
|
(isArgumentSuggestion ? "" : COMMAND_HINTS[item.name] ?? item.description);
|
|
124
|
-
const name = item.name
|
|
125
|
-
const desc =
|
|
140
|
+
const name = padDisplayText(item.name, nameBudget);
|
|
141
|
+
const desc = padDisplayText(item.description, descBudget);
|
|
126
142
|
const clippedHint =
|
|
127
143
|
hintBudget > 0 ? fitDisplayText(hint, hintBudget) : "";
|
|
128
|
-
const rowWidth =
|
|
129
|
-
2 +
|
|
130
|
-
displayWidth(name) +
|
|
131
|
-
1 +
|
|
132
|
-
displayWidth(desc) +
|
|
133
|
-
(clippedHint ? 2 + displayWidth(clippedHint) : 0);
|
|
134
144
|
return (
|
|
135
|
-
<Box key={item.name} width={
|
|
145
|
+
<Box key={item.name} width={innerWidth} flexShrink={1}>
|
|
136
146
|
<Text color={sel ? t.primaryLight : t.primaryDim} bold={sel}>
|
|
137
147
|
{sel ? "› " : " "}
|
|
138
148
|
</Text>
|
|
@@ -140,7 +150,7 @@ function CommandPaletteInner({ items, selectedIdx, width = 80 }: Props) {
|
|
|
140
150
|
{name}
|
|
141
151
|
</Text>
|
|
142
152
|
<Text color={t.primaryDim}> </Text>
|
|
143
|
-
<Text color={sel ? t.text : t.dim}
|
|
153
|
+
<Text color={sel ? t.text : t.dim}>
|
|
144
154
|
{desc}
|
|
145
155
|
</Text>
|
|
146
156
|
{clippedHint ? (
|