drexler 0.2.9 → 0.2.10

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 CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.10
4
+
5
+ - Closed transcript turn blocks with right-side borders and corners so user and Drexler cards align with the input frame.
6
+ - Aligned the status row with the main chat chrome.
7
+
3
8
  ## 0.2.9
4
9
 
5
10
  - Removed transcript card side labels like `incoming memo` and `response ledger`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drexler",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "description": "CLI chat with Drexler, a corporate-executive AI persona built on OpenRouter Gemma 4 31B.",
5
5
  "license": "MIT",
6
6
  "author": "showOS",
package/src/ui/App.tsx CHANGED
@@ -124,7 +124,7 @@ export function App({ conversation, config, mood = "neutral", fetchFn }: AppProp
124
124
  const mode = useMemo(() => pickLayout(cols), [cols]);
125
125
  const inputWidth = useMemo(() => Math.max(1, cols), [cols]);
126
126
  const chromeWidth = useMemo(() => Math.max(1, cols), [cols]);
127
- const statusBarWidth = useMemo(() => Math.max(1, inputWidth - 2), [inputWidth]);
127
+ const statusBarWidth = inputWidth;
128
128
  const isCompact = mode === "very-narrow";
129
129
  const maxTranscriptRows = useMemo(
130
130
  () => transcriptRowsForTerminalRows(rows),
@@ -303,7 +303,7 @@ export function App({ conversation, config, mood = "neutral", fetchFn }: AppProp
303
303
  setWitticism(event.finalLine);
304
304
  addItem("system", event.transcriptLine);
305
305
  }
306
- }, 60);
306
+ }, 45);
307
307
  }, [addItem]);
308
308
 
309
309
  const runLLM = useCallback(async (instruction?: string) => {
@@ -776,7 +776,7 @@ export function App({ conversation, config, mood = "neutral", fetchFn }: AppProp
776
776
  width={inputWidth}
777
777
  />
778
778
  </Box>
779
- <Box paddingLeft={2}>
779
+ <Box>
780
780
  <StatusBar
781
781
  messageCount={msgCount}
782
782
  witticism={witticism}
@@ -1,5 +1,5 @@
1
1
  import { Box, Text } from "ink";
2
- import { memo } from "react";
2
+ import { memo, useMemo } from "react";
3
3
  import { displayWidth, fitDisplayText } from "./graphemes.ts";
4
4
  import { useTheme } from "./ThemeContext.tsx";
5
5
 
@@ -142,6 +142,17 @@ function frameProgress(frame: number): number {
142
142
  return Math.max(0, Math.min(1, frame / (SYNERGY_EVENT_FRAMES - 1)));
143
143
  }
144
144
 
145
+ // Cubic ease-out: fast reveal, soft settle. Feels less mechanical than linear.
146
+ function easeOutCubic(p: number): number {
147
+ const clamped = Math.max(0, Math.min(1, p));
148
+ const inv = 1 - clamped;
149
+ return 1 - inv * inv * inv;
150
+ }
151
+
152
+ function easedProgress(frame: number): number {
153
+ return easeOutCubic(frameProgress(frame));
154
+ }
155
+
145
156
  function bar(progress: number, width: number): string {
146
157
  const safeWidth = Math.max(1, width);
147
158
  const filled = Math.max(0, Math.min(safeWidth, Math.round(progress * safeWidth)));
@@ -149,7 +160,7 @@ function bar(progress: number, width: number): string {
149
160
  }
150
161
 
151
162
  function stageAt(event: SynergyEventDefinition, frame: number): string {
152
- const progress = frameProgress(frame);
163
+ const progress = easedProgress(frame);
153
164
  const idx = Math.min(
154
165
  event.stages.length - 1,
155
166
  Math.floor(progress * event.stages.length),
@@ -157,13 +168,10 @@ function stageAt(event: SynergyEventDefinition, frame: number): string {
157
168
  return event.stages[idx]!;
158
169
  }
159
170
 
160
- function visibleArt(event: SynergyEventDefinition, frame: number): readonly string[] {
161
- const progress = frameProgress(frame);
162
- const count = Math.max(
163
- 1,
164
- Math.ceil(progress * Math.min(event.art.length, FULL_EVENT_ART_ROWS)),
165
- );
166
- return event.art.slice(0, count);
171
+ function visibleArtCount(event: SynergyEventDefinition, frame: number): number {
172
+ const progress = easedProgress(frame);
173
+ const cap = Math.min(event.art.length, FULL_EVENT_ART_ROWS);
174
+ return Math.max(1, Math.ceil(progress * cap));
167
175
  }
168
176
 
169
177
  function kpiAt(event: SynergyEventDefinition, frame: number): string {
@@ -185,9 +193,18 @@ function SynergyEventInner({
185
193
  }: Props) {
186
194
  const t = useTheme();
187
195
  const safeWidth = Math.max(1, Math.floor(width));
188
- const progress = frameProgress(frame);
196
+ const progress = easedProgress(frame);
189
197
  const done = frame >= SYNERGY_EVENT_FRAMES - 1;
190
198
  const tiny = safeWidth < 38 || compact;
199
+ // Title pulse: alert-style flicker during early reveal, calm bright on done.
200
+ const earlyPulse = !done && frame < 6 && frame % 2 === 0;
201
+ const titleColor = done ? t.primaryLight : earlyPulse ? t.primary : t.warning;
202
+
203
+ const artFrames = useMemo(
204
+ () => event.art.slice(0, FULL_EVENT_ART_ROWS),
205
+ [event],
206
+ );
207
+ const revealedRows = visibleArtCount(event, frame);
191
208
 
192
209
  if (tiny) {
193
210
  const label = done ? event.finalLine : stageAt(event, frame);
@@ -195,7 +212,7 @@ function SynergyEventInner({
195
212
  const line = `SYNC ${bar(progress, miniBarWidth)} ${label}`;
196
213
  return (
197
214
  <Box width={safeWidth} flexShrink={1}>
198
- <Text color={done ? t.primaryLight : t.warning} bold wrap="truncate">
215
+ <Text color={titleColor} bold wrap="truncate">
199
216
  {fitDisplayText(line, safeWidth)}
200
217
  </Text>
201
218
  </Box>
@@ -223,7 +240,7 @@ function SynergyEventInner({
223
240
  flexShrink={1}
224
241
  >
225
242
  <Box>
226
- <Text color={t.warning} bold>
243
+ <Text color={titleColor} bold>
227
244
  SYNERGY EVENT
228
245
  </Text>
229
246
  <Text color={t.primaryDim}> ─ </Text>
@@ -232,8 +249,8 @@ function SynergyEventInner({
232
249
  </Text>
233
250
  </Box>
234
251
  <Box flexDirection="column">
235
- {event.art.slice(0, FULL_EVENT_ART_ROWS).map((line, idx) => {
236
- const revealed = idx < visibleArt(event, frame).length;
252
+ {artFrames.map((line, idx) => {
253
+ const revealed = idx < revealedRows;
237
254
  return (
238
255
  <Text
239
256
  key={`${event.id}-${idx}`}
@@ -103,8 +103,12 @@ function itemRows(
103
103
  cols: number,
104
104
  ): number {
105
105
  if (compact) return 1;
106
- const bodyPrefix = `${ROLE_MARKERS[item.role]} `;
107
- const contentWidth = Math.max(1, cols - displayWidth(bodyPrefix));
106
+ const bodyPrefix = "│ › ";
107
+ const bodySuffix = " │";
108
+ const contentWidth = Math.max(
109
+ 1,
110
+ cols - displayWidth(bodyPrefix) - displayWidth(bodySuffix),
111
+ );
108
112
  return 2 + wrappedContentRows(item.content, contentWidth).length;
109
113
  }
110
114
 
@@ -162,31 +166,41 @@ function DefaultTranscriptItem({
162
166
  }
163
167
 
164
168
  const headerPrefix = `╭─ ${label} `;
165
- const headerRuleWidth = Math.max(0, cols - displayWidth(headerPrefix));
166
- const footerWidth = Math.max(1, cols - 1);
167
- const bodyPrefix = `${ROLE_MARKERS[item.role]} `;
168
- const contentWidth = Math.max(1, cols - displayWidth(bodyPrefix));
169
+ const headerRuleWidth = Math.max(0, cols - displayWidth(headerPrefix) - 1);
170
+ const footerWidth = Math.max(0, cols - 2);
171
+ const bodyPrefix = item.role === "user" ? "│ › " : "│ ";
172
+ const continuationPrefix = "│ ";
173
+ const bodySuffix = " │";
174
+ const contentWidth = Math.max(
175
+ 1,
176
+ cols - displayWidth(bodyPrefix) - displayWidth(bodySuffix),
177
+ );
169
178
 
170
179
  return (
171
180
  <Box flexDirection="column" width={cols} flexShrink={1}>
172
181
  <Text color={accent} bold wrap="truncate">
173
182
  {fitDisplayText(
174
- `${headerPrefix}${rule("─", headerRuleWidth)}`,
183
+ `${headerPrefix}${rule("─", headerRuleWidth)}╮`,
175
184
  cols,
176
185
  )}
177
186
  </Text>
178
187
  {wrappedContentRows(item.content, contentWidth).map((line, index) => (
179
188
  <Box key={index} width={cols} flexShrink={1}>
180
189
  <Text color={accent} bold={item.role === "user"}>
181
- {index === 0 || item.role === "assistant" ? bodyPrefix : " "}
190
+ {index === 0 ? bodyPrefix : continuationPrefix}
182
191
  </Text>
183
192
  <Text color={roleBodyColor(item.role, t)}>
184
- {line}
193
+ {fitDisplayText(line, contentWidth)}
194
+ </Text>
195
+ <Text color={accent} bold={item.role === "user"}>
196
+ {`${" ".repeat(
197
+ Math.max(0, contentWidth - displayWidth(line)),
198
+ )}${bodySuffix}`}
185
199
  </Text>
186
200
  </Box>
187
201
  ))}
188
202
  <Text color={accent} bold={item.role === "user"} wrap="truncate">
189
- {fitDisplayText(`╰${rule("─", footerWidth)}`, cols)}
203
+ {fitDisplayText(`╰${rule("─", footerWidth)}╯`, cols)}
190
204
  </Text>
191
205
  </Box>
192
206
  );