drexler 0.2.7 → 0.2.8

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.8
4
+
5
+ - Improved transcript readability by wrapping long user and Drexler message lines instead of truncating them.
6
+ - Kept wrapped continuation rows visually aligned inside the existing turn blocks.
7
+
3
8
  ## 0.2.7
4
9
 
5
10
  - Stabilized `/synergy` animation layout with fixed row budgeting, a capped centered event panel, and completion only at 100%.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drexler",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
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",
@@ -1,6 +1,6 @@
1
1
  import { Box, Text } from "ink";
2
2
  import { Children, memo, useMemo, type ReactNode } from "react";
3
- import { displayWidth, fitDisplayText } from "./graphemes.ts";
3
+ import { displayWidth, fitDisplayText, splitGraphemes } from "./graphemes.ts";
4
4
  import { useTheme } from "./ThemeContext.tsx";
5
5
 
6
6
  export interface TranscriptViewportItem {
@@ -47,14 +47,71 @@ const ROLE_MARKERS: Record<TranscriptViewportItem["role"], string> = {
47
47
  system: "!",
48
48
  };
49
49
 
50
- function lineCount(input: string): number {
51
- if (input.length === 0) return 1;
52
- return input.split("\n").length;
50
+ function wrapDisplayLine(input: string, maxWidth: number): string[] {
51
+ const width = Math.max(1, maxWidth);
52
+ if (input.length === 0) return [""];
53
+ if (displayWidth(input) <= width) return [input];
54
+
55
+ const parts = splitGraphemes(input);
56
+ const rows: string[] = [];
57
+ let current = "";
58
+ let lastBreakAt = -1;
59
+
60
+ for (const part of parts) {
61
+ const next = `${current}${part}`;
62
+ if (displayWidth(next) <= width) {
63
+ current = next;
64
+ if (/\s/u.test(part)) lastBreakAt = current.length;
65
+ continue;
66
+ }
67
+
68
+ if (lastBreakAt > 0) {
69
+ const head = current.slice(0, lastBreakAt).trimEnd();
70
+ const tail = current.slice(lastBreakAt).trimStart();
71
+ rows.push(head);
72
+ current = `${tail}${part}`;
73
+ } else {
74
+ if (current.length > 0) rows.push(current);
75
+ current = part;
76
+ }
77
+ lastBreakAt = /\s/u.test(part) ? current.length : -1;
78
+
79
+ while (displayWidth(current) > width) {
80
+ let clipped = "";
81
+ for (const grapheme of splitGraphemes(current)) {
82
+ if (displayWidth(`${clipped}${grapheme}`) > width) break;
83
+ clipped += grapheme;
84
+ }
85
+ if (clipped.length === 0) {
86
+ const [first = ""] = splitGraphemes(current);
87
+ rows.push(fitDisplayText(first, width));
88
+ current = current.slice(first.length);
89
+ continue;
90
+ }
91
+ rows.push(clipped);
92
+ current = current.slice(clipped.length);
93
+ }
94
+ }
95
+
96
+ rows.push(current.trimEnd());
97
+ return rows.filter((row, index) => row.length > 0 || index === 0);
98
+ }
99
+
100
+ function wrappedContentRows(content: string, width: number): string[] {
101
+ return content
102
+ .split("\n")
103
+ .flatMap((line) => wrapDisplayLine(line, width));
53
104
  }
54
105
 
55
- function itemRows(item: TranscriptViewportItem, compact: boolean): number {
106
+ function itemRows(
107
+ item: TranscriptViewportItem,
108
+ compact: boolean,
109
+ cols: number,
110
+ ): number {
56
111
  if (compact) return 1;
57
- return 2 + lineCount(item.content);
112
+ const bodyPrefix = `${ROLE_MARKERS[item.role]} `;
113
+ const contentWidth = Math.max(1, cols - displayWidth(bodyPrefix));
114
+ return 2 + wrappedContentRows(item.content, contentWidth).length;
58
115
  }
59
116
 
60
117
  function roleAccentColor(
@@ -129,13 +186,13 @@ function DefaultTranscriptItem({
129
186
  cols,
130
187
  )}
131
188
  </Text>
132
- {item.content.split("\n").map((line, index) => (
189
+ {wrappedContentRows(item.content, contentWidth).map((line, index) => (
133
190
  <Box key={index} width={cols} flexShrink={1}>
134
191
  <Text color={accent} bold={item.role === "user"}>
135
- {bodyPrefix}
192
+ {index === 0 || item.role === "assistant" ? bodyPrefix : " "}
136
193
  </Text>
137
- <Text color={roleBodyColor(item.role, t)} wrap="truncate">
138
- {fitDisplayText(line, contentWidth)}
194
+ <Text color={roleBodyColor(item.role, t)}>
195
+ {line}
139
196
  </Text>
140
197
  </Box>
141
198
  ))}
@@ -172,7 +229,7 @@ function itemsToEntries({
172
229
  ) : (
173
230
  <DefaultTranscriptItem item={item} compact={compact} cols={cols} />
174
231
  ),
175
- estimatedRows: itemRows(item, compact),
232
+ estimatedRows: itemRows(item, compact, cols),
176
233
  }));
177
234
  }
178
235