drexler 0.2.13 → 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 +8 -0
- package/README.md +58 -13
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/ui/App.tsx +16 -4
- package/src/ui/DealDeskHeader.tsx +245 -72
- package/src/ui/MarkdownBody.tsx +382 -0
- package/src/ui/MascotIntro.tsx +573 -72
- package/src/ui/Message.tsx +28 -15
- package/src/ui/SynergyEvent.tsx +3 -2
- package/src/ui/TranscriptViewport.tsx +258 -25
- package/src/ui/displayContent.ts +114 -0
- package/src/ui/graphemes.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
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
|
+
|
|
3
11
|
## 0.2.13
|
|
4
12
|
|
|
5
13
|
- Hardened startup panel layout across narrow, standard, and wide terminals.
|
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";
|
|
@@ -204,6 +208,7 @@ export function App({
|
|
|
204
208
|
const [historyIdx, setHistoryIdx] = useState<number | null>(null);
|
|
205
209
|
const [paletteIdx, setPaletteIdx] = useState(0);
|
|
206
210
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
211
|
+
const intro = useIntroAnimation(chromeWidth, integratedIntro);
|
|
207
212
|
|
|
208
213
|
const paletteItems = useMemo(() => filterPaletteByPrefix(input), [input]);
|
|
209
214
|
const paletteOpen = paletteItems.length > 0;
|
|
@@ -731,7 +736,7 @@ export function App({
|
|
|
731
736
|
status={headerStatus}
|
|
732
737
|
compact={isCompact}
|
|
733
738
|
notice={!integratedIntro ? deskNotice ?? undefined : undefined}
|
|
734
|
-
maxWidth={
|
|
739
|
+
maxWidth={Math.max(1, width)}
|
|
735
740
|
marginBottom={integratedIntro ? 0 : 1}
|
|
736
741
|
/>
|
|
737
742
|
);
|
|
@@ -739,6 +744,7 @@ export function App({
|
|
|
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,6 +754,12 @@ export function App({
|
|
|
748
754
|
<MascotDashboard
|
|
749
755
|
greeting={greeting}
|
|
750
756
|
width={chromeWidth}
|
|
757
|
+
mood={mood}
|
|
758
|
+
bootProgress={intro.progress}
|
|
759
|
+
state={intro.state}
|
|
760
|
+
bar={intro.bar}
|
|
761
|
+
barColor={introBarColor}
|
|
762
|
+
mascotStatus={intro.status}
|
|
751
763
|
dealDesk={renderDealDeskHeader}
|
|
752
764
|
/>
|
|
753
765
|
</Box>
|
|
@@ -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}
|
|
@@ -24,10 +24,81 @@ const DEFAULT_WIDTH = 80;
|
|
|
24
24
|
const MIN_WIDTH = 1;
|
|
25
25
|
const FRAMED_MIN_WIDTH = 24;
|
|
26
26
|
|
|
27
|
-
const
|
|
28
|
-
idle: "
|
|
29
|
-
streaming: "LIVE",
|
|
30
|
-
error: "
|
|
27
|
+
const BOARDROOM_STATUS: Record<DealDeskHeaderStatus, string> = {
|
|
28
|
+
idle: "BOARDROOM OPEN",
|
|
29
|
+
streaming: "MEMO LIVE",
|
|
30
|
+
error: "COUNSEL PANIC",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type DealDeskPoolKey =
|
|
34
|
+
| "fees"
|
|
35
|
+
| "mandate"
|
|
36
|
+
| "risk"
|
|
37
|
+
| "counsel"
|
|
38
|
+
| "morale"
|
|
39
|
+
| "synergy";
|
|
40
|
+
|
|
41
|
+
const DEFAULT_POOL: Record<DealDeskPoolKey, readonly string[]> = {
|
|
42
|
+
fees: ["accruing", "sacred", "non-refundable", "already billed"],
|
|
43
|
+
mandate: ["self-awarded", "board-adjacent", "strategic-ish", "lightly authorized"],
|
|
44
|
+
risk: ["theatrical", "outsourced", "priced in", "someone else's"],
|
|
45
|
+
counsel: ["circling", "evasive", "comfortable", "redlining lunch"],
|
|
46
|
+
morale: ["impaired", "marked down", "technically solvent", "under review"],
|
|
47
|
+
synergy: ["alleged", "unverifiable", "already billed", "pending lawsuit"],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const MOOD_POOLS: Record<
|
|
51
|
+
string,
|
|
52
|
+
Partial<Record<DealDeskPoolKey, readonly string[]>>
|
|
53
|
+
> = {
|
|
54
|
+
angry: {
|
|
55
|
+
fees: ["weaponized", "escalating", "aggressively earned", "non-refundable"],
|
|
56
|
+
mandate: ["hostile", "loudly implied", "board-threatening", "self-ratified"],
|
|
57
|
+
risk: ["acceptable", "transferred", "career-limiting", "somebody else's"],
|
|
58
|
+
counsel: ["circling", "sweating", "denying knowledge", "overruled"],
|
|
59
|
+
morale: ["terminated", "impaired", "written off", "reassigned"],
|
|
60
|
+
synergy: ["forced", "mandatory", "hostile", "already billed"],
|
|
61
|
+
},
|
|
62
|
+
exhausted: {
|
|
63
|
+
fees: ["still accruing", "quietly sacred", "tired but billable", "unquestioned"],
|
|
64
|
+
mandate: ["unclear", "half-approved", "forgotten", "pending coffee"],
|
|
65
|
+
risk: ["deferred", "sleepy", "filed tomorrow", "emotionally hedged"],
|
|
66
|
+
counsel: ["unavailable", "out of office", "blinking slowly", "circling"],
|
|
67
|
+
morale: ["written off", "napping", "below guidance", "technically awake"],
|
|
68
|
+
synergy: ["alleged", "too tired to verify", "softly promised", "unfunded"],
|
|
69
|
+
},
|
|
70
|
+
paranoid: {
|
|
71
|
+
fees: ["traced", "escrowed twice", "suspiciously round", "under seal"],
|
|
72
|
+
mandate: ["encrypted", "deniable", "need-to-know", "redacted"],
|
|
73
|
+
risk: ["everywhere", "listening", "unhedged", "wearing a wire"],
|
|
74
|
+
counsel: ["whispering", "triple-checking", "using burner phones", "redacting"],
|
|
75
|
+
morale: ["surveilled", "compartmentalized", "need-to-know", "encrypted"],
|
|
76
|
+
synergy: ["classified", "denied", "redacted", "not in minutes"],
|
|
77
|
+
},
|
|
78
|
+
generous: {
|
|
79
|
+
fees: ["shared emotionally", "still ours", "politely accruing", "gift-wrapped"],
|
|
80
|
+
mandate: ["benevolent", "magnanimous", "soft hostile", "board-blessed"],
|
|
81
|
+
risk: ["forgiven", "socialized", "gently transferred", "nicely hedged"],
|
|
82
|
+
counsel: ["agreeable", "smiling carefully", "comfortable", "charitable"],
|
|
83
|
+
morale: ["briefly up", "subsidized", "pleasantly marked", "gifted options"],
|
|
84
|
+
synergy: ["donated", "mutual-ish", "kindly alleged", "complimentary"],
|
|
85
|
+
},
|
|
86
|
+
ruthless: {
|
|
87
|
+
fees: ["sacred", "extractive", "fully captured", "compounding"],
|
|
88
|
+
mandate: ["hostile", "absolute", "self-awarded", "non-appealable"],
|
|
89
|
+
risk: ["outsourced", "priced in", "assigned to interns", "deleted"],
|
|
90
|
+
counsel: ["overpaid", "comfortable", "aggressively calm", "circling"],
|
|
91
|
+
morale: ["impaired", "irrelevant", "restructured", "sold separately"],
|
|
92
|
+
synergy: ["mandatory", "already billed", "non-consensual", "accretive"],
|
|
93
|
+
},
|
|
94
|
+
victorious: {
|
|
95
|
+
fees: ["captured", "celebrated", "fully earned", "ringing bell"],
|
|
96
|
+
mandate: ["ratified", "triumphant", "board-crowned", "unopposed"],
|
|
97
|
+
risk: ["conquered", "renamed upside", "priced in", "defeated"],
|
|
98
|
+
counsel: ["applauding", "comfortable", "drafting trophies", "filing confetti"],
|
|
99
|
+
morale: ["temporarily high", "marked up", "wearing medals", "overstated"],
|
|
100
|
+
synergy: ["declared", "victorious", "already billed", "banner-ready"],
|
|
101
|
+
},
|
|
31
102
|
};
|
|
32
103
|
|
|
33
104
|
function clampText(input: string, max: number): string {
|
|
@@ -51,98 +122,194 @@ function bodyLine(content: string, width: number): string {
|
|
|
51
122
|
return `│ ${padToWidth(clampText(content, innerWidth), innerWidth)} │`;
|
|
52
123
|
}
|
|
53
124
|
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
return `${messageCount}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function latencyLabel(latencyMs: number | null | undefined): string | null {
|
|
60
|
-
if (typeof latencyMs !== "number") return null;
|
|
61
|
-
if (latencyMs < 1000) return `${Math.max(0, Math.round(latencyMs))}ms`;
|
|
62
|
-
return `${(latencyMs / 1000).toFixed(1)}s`;
|
|
125
|
+
function memoLabel(messageCount: number): string {
|
|
126
|
+
const noun = "memo";
|
|
127
|
+
return `${messageCount} ${noun}${messageCount === 1 ? "" : "s"}`;
|
|
63
128
|
}
|
|
64
129
|
|
|
65
130
|
function tinyLine({
|
|
66
|
-
model,
|
|
67
131
|
messageCount,
|
|
68
132
|
status,
|
|
69
133
|
width,
|
|
70
134
|
}: {
|
|
71
|
-
model: string;
|
|
72
135
|
messageCount: number;
|
|
73
136
|
status: DealDeskHeaderStatus;
|
|
74
137
|
width: number;
|
|
75
138
|
}): string {
|
|
76
139
|
return clampText(
|
|
77
|
-
`${
|
|
140
|
+
`${BOARDROOM_STATUS[status]} ${memoLabel(messageCount)}`,
|
|
78
141
|
width,
|
|
79
142
|
);
|
|
80
143
|
}
|
|
81
144
|
|
|
145
|
+
function pickFromMoodPool({
|
|
146
|
+
key,
|
|
147
|
+
mood,
|
|
148
|
+
salt,
|
|
149
|
+
}: {
|
|
150
|
+
key: DealDeskPoolKey;
|
|
151
|
+
mood: string;
|
|
152
|
+
salt: number;
|
|
153
|
+
}): string {
|
|
154
|
+
const pool = MOOD_POOLS[mood.toLowerCase()]?.[key] ?? DEFAULT_POOL[key];
|
|
155
|
+
return pool[Math.abs(salt) % pool.length] ?? DEFAULT_POOL[key][0];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function hashDealDesk(input: string): number {
|
|
159
|
+
let hash = 2166136261;
|
|
160
|
+
for (let idx = 0; idx < input.length; idx++) {
|
|
161
|
+
hash ^= input.charCodeAt(idx);
|
|
162
|
+
hash = Math.imul(hash, 16777619);
|
|
163
|
+
}
|
|
164
|
+
return hash >>> 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function formatCells(cells: string[], width: number): string {
|
|
168
|
+
const separator = " │ ";
|
|
169
|
+
const available = Math.max(
|
|
170
|
+
1,
|
|
171
|
+
width - displayWidth(separator) * Math.max(0, cells.length - 1),
|
|
172
|
+
);
|
|
173
|
+
const base = Math.max(1, Math.floor(available / cells.length));
|
|
174
|
+
const remainder = Math.max(0, available - base * cells.length);
|
|
175
|
+
return cells
|
|
176
|
+
.map((cell, idx) => {
|
|
177
|
+
const cellWidth = base + (idx < remainder ? 1 : 0);
|
|
178
|
+
return padToWidth(clampText(cell, cellWidth), cellWidth);
|
|
179
|
+
})
|
|
180
|
+
.join(separator);
|
|
181
|
+
}
|
|
182
|
+
|
|
82
183
|
function buildHeaderLines({
|
|
83
|
-
model,
|
|
84
184
|
mood,
|
|
85
185
|
messageCount,
|
|
86
|
-
themeName,
|
|
87
|
-
approximateTokens,
|
|
88
|
-
latencyMs,
|
|
89
|
-
fallbackModel,
|
|
90
186
|
status,
|
|
91
187
|
compact,
|
|
92
188
|
notice,
|
|
93
189
|
width,
|
|
190
|
+
seed,
|
|
94
191
|
}: {
|
|
95
|
-
model: string;
|
|
96
192
|
mood: string;
|
|
97
193
|
messageCount: number;
|
|
98
|
-
themeName?: string;
|
|
99
|
-
approximateTokens?: number;
|
|
100
|
-
latencyMs?: number | null;
|
|
101
|
-
fallbackModel?: string | null;
|
|
102
194
|
status: DealDeskHeaderStatus;
|
|
103
195
|
compact: boolean;
|
|
104
196
|
notice?: string;
|
|
105
197
|
width: number;
|
|
198
|
+
seed: number;
|
|
106
199
|
}): string[] {
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
200
|
+
const innerWidth = Math.max(1, width - 4);
|
|
201
|
+
const baseHash = hashDealDesk(`${mood}:${messageCount}:${status}:${seed}`);
|
|
202
|
+
const pick = (key: DealDeskPoolKey, offset: number) =>
|
|
203
|
+
pickFromMoodPool({ key, mood, salt: baseHash + offset * 7919 });
|
|
204
|
+
const statusLabel = BOARDROOM_STATUS[status];
|
|
113
205
|
const summary = compact
|
|
114
|
-
?
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
206
|
+
? formatCells(
|
|
207
|
+
[`● ${statusLabel}`, `mood ${mood}`, `fees ${pick("fees", 1)}`],
|
|
208
|
+
innerWidth,
|
|
209
|
+
)
|
|
210
|
+
: formatCells(
|
|
211
|
+
[
|
|
212
|
+
`● ${statusLabel}`,
|
|
213
|
+
memoLabel(messageCount),
|
|
214
|
+
`fees ${pick("fees", 1)}`,
|
|
215
|
+
],
|
|
216
|
+
innerWidth,
|
|
217
|
+
);
|
|
218
|
+
const readout = compact
|
|
219
|
+
? formatCells(
|
|
220
|
+
[`risk ${pick("risk", 2)}`, `counsel ${pick("counsel", 3)}`],
|
|
221
|
+
innerWidth,
|
|
222
|
+
)
|
|
223
|
+
: formatCells(
|
|
224
|
+
[
|
|
225
|
+
`mandate ${pick("mandate", 4)}`,
|
|
226
|
+
`risk ${pick("risk", 5)}`,
|
|
227
|
+
`counsel ${pick("counsel", 6)}`,
|
|
228
|
+
],
|
|
229
|
+
innerWidth,
|
|
230
|
+
);
|
|
231
|
+
const lines = [bodyLine(summary, width), bodyLine(readout, width)];
|
|
129
232
|
|
|
130
233
|
if (!compact && notice && notice.trim().length > 0) {
|
|
131
|
-
|
|
234
|
+
const memo = formatCells(
|
|
235
|
+
[
|
|
236
|
+
`memo ${notice.trim()}`,
|
|
237
|
+
`morale ${pick("morale", 7)}`,
|
|
238
|
+
`synergy ${pick("synergy", 8)}`,
|
|
239
|
+
],
|
|
240
|
+
innerWidth,
|
|
241
|
+
);
|
|
242
|
+
lines.push(bodyLine(memo, width));
|
|
132
243
|
}
|
|
133
244
|
|
|
134
|
-
lines.push(shellLine("
|
|
245
|
+
lines.push(shellLine("╰", "╯", width));
|
|
135
246
|
return lines;
|
|
136
247
|
}
|
|
137
248
|
|
|
249
|
+
function titleLabel(compact: boolean): string {
|
|
250
|
+
return compact ? "Drexler" : "Drexler Deal Desk";
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function FramedTitleText({
|
|
254
|
+
compact,
|
|
255
|
+
borderColor,
|
|
256
|
+
titleColor,
|
|
257
|
+
width,
|
|
258
|
+
}: {
|
|
259
|
+
compact: boolean;
|
|
260
|
+
borderColor: string;
|
|
261
|
+
titleColor: string;
|
|
262
|
+
width: number;
|
|
263
|
+
}) {
|
|
264
|
+
const title = titleLabel(compact);
|
|
265
|
+
const prefix = "╭─ ";
|
|
266
|
+
const titleSuffix = " ";
|
|
267
|
+
const suffix = "╮";
|
|
268
|
+
const ruleWidth = Math.max(
|
|
269
|
+
0,
|
|
270
|
+
width -
|
|
271
|
+
displayWidth(prefix) -
|
|
272
|
+
displayWidth(title) -
|
|
273
|
+
displayWidth(titleSuffix) -
|
|
274
|
+
displayWidth(suffix),
|
|
275
|
+
);
|
|
276
|
+
return (
|
|
277
|
+
<Text>
|
|
278
|
+
<Text color={borderColor}>{prefix}</Text>
|
|
279
|
+
<Text bold color={titleColor}>
|
|
280
|
+
{title}
|
|
281
|
+
</Text>
|
|
282
|
+
<Text color={borderColor}>
|
|
283
|
+
{titleSuffix}
|
|
284
|
+
{"─".repeat(ruleWidth)}
|
|
285
|
+
{suffix}
|
|
286
|
+
</Text>
|
|
287
|
+
</Text>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function FramedBodyText({
|
|
292
|
+
line,
|
|
293
|
+
borderColor,
|
|
294
|
+
contentColor,
|
|
295
|
+
}: {
|
|
296
|
+
line: string;
|
|
297
|
+
borderColor: string;
|
|
298
|
+
contentColor: string;
|
|
299
|
+
}) {
|
|
300
|
+
const content = line.length >= 4 ? line.slice(2, -2) : line;
|
|
301
|
+
return (
|
|
302
|
+
<Text>
|
|
303
|
+
<Text color={borderColor}>│ </Text>
|
|
304
|
+
<Text color={contentColor}>{content}</Text>
|
|
305
|
+
<Text color={borderColor}> │</Text>
|
|
306
|
+
</Text>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
138
310
|
function DealDeskHeaderInner({
|
|
139
|
-
model,
|
|
140
311
|
mood,
|
|
141
312
|
messageCount,
|
|
142
|
-
themeName,
|
|
143
|
-
approximateTokens,
|
|
144
|
-
latencyMs,
|
|
145
|
-
fallbackModel,
|
|
146
313
|
status = "idle",
|
|
147
314
|
compact = false,
|
|
148
315
|
notice,
|
|
@@ -151,6 +318,7 @@ function DealDeskHeaderInner({
|
|
|
151
318
|
}: DealDeskHeaderProps) {
|
|
152
319
|
const t = useTheme();
|
|
153
320
|
const width = Math.max(MIN_WIDTH, Math.floor(maxWidth));
|
|
321
|
+
const randomSeed = useMemo(() => Math.floor(Math.random() * 1_000_000_000), []);
|
|
154
322
|
const statusColor: Record<DealDeskHeaderStatus, string> = useMemo(
|
|
155
323
|
() => ({
|
|
156
324
|
idle: t.primaryLight,
|
|
@@ -159,32 +327,25 @@ function DealDeskHeaderInner({
|
|
|
159
327
|
}),
|
|
160
328
|
[t.error, t.primaryLight, t.warning],
|
|
161
329
|
);
|
|
330
|
+
const summaryColor = status === "idle" ? t.text : statusColor[status];
|
|
162
331
|
const lines = useMemo(
|
|
163
332
|
() =>
|
|
164
333
|
buildHeaderLines({
|
|
165
|
-
model,
|
|
166
334
|
mood,
|
|
167
335
|
messageCount,
|
|
168
|
-
themeName,
|
|
169
|
-
approximateTokens,
|
|
170
|
-
latencyMs,
|
|
171
|
-
fallbackModel,
|
|
172
336
|
status,
|
|
173
337
|
compact,
|
|
174
338
|
notice,
|
|
175
339
|
width,
|
|
340
|
+
seed: randomSeed,
|
|
176
341
|
}),
|
|
177
342
|
[
|
|
178
|
-
approximateTokens,
|
|
179
343
|
compact,
|
|
180
|
-
fallbackModel,
|
|
181
|
-
latencyMs,
|
|
182
344
|
messageCount,
|
|
183
|
-
model,
|
|
184
345
|
mood,
|
|
185
346
|
notice,
|
|
347
|
+
randomSeed,
|
|
186
348
|
status,
|
|
187
|
-
themeName,
|
|
188
349
|
width,
|
|
189
350
|
],
|
|
190
351
|
);
|
|
@@ -193,7 +354,7 @@ function DealDeskHeaderInner({
|
|
|
193
354
|
return (
|
|
194
355
|
<Box width={width} marginBottom={marginBottom}>
|
|
195
356
|
<Text color={statusColor[status]} wrap="truncate">
|
|
196
|
-
{tinyLine({
|
|
357
|
+
{tinyLine({ messageCount, status, width })}
|
|
197
358
|
</Text>
|
|
198
359
|
</Box>
|
|
199
360
|
);
|
|
@@ -201,14 +362,26 @@ function DealDeskHeaderInner({
|
|
|
201
362
|
|
|
202
363
|
return (
|
|
203
364
|
<Box flexDirection="column" width={width} marginBottom={marginBottom}>
|
|
204
|
-
<
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
365
|
+
<FramedTitleText
|
|
366
|
+
compact={compact}
|
|
367
|
+
borderColor={t.primary}
|
|
368
|
+
titleColor={t.primaryLight}
|
|
369
|
+
width={width}
|
|
370
|
+
/>
|
|
371
|
+
<FramedBodyText
|
|
372
|
+
line={lines[0] ?? ""}
|
|
373
|
+
borderColor={t.primary}
|
|
374
|
+
contentColor={summaryColor}
|
|
375
|
+
/>
|
|
376
|
+
{lines.slice(1, -1).map((line, index) => (
|
|
377
|
+
<FramedBodyText
|
|
378
|
+
key={index}
|
|
379
|
+
line={line}
|
|
380
|
+
borderColor={t.primary}
|
|
381
|
+
contentColor={index === 0 ? t.text : t.dim}
|
|
382
|
+
/>
|
|
210
383
|
))}
|
|
211
|
-
<Text color={t.
|
|
384
|
+
<Text color={t.primary}>{lines[lines.length - 1]}</Text>
|
|
212
385
|
</Box>
|
|
213
386
|
);
|
|
214
387
|
}
|