ac-teams 0.0.16 → 0.0.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ac-teams",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "description": "Small, persistent teams for Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/panel.ts CHANGED
@@ -65,6 +65,7 @@ export async function showTeamsPanel(ctx: ExtensionContext, host: PanelHost): Pr
65
65
  return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
66
66
  };
67
67
  const heading = (text: string) => theme.bold(fg("text", text));
68
+ const tableHeader = (text: string) => theme.bold(theme.underline(fg("accent", text)));
68
69
  const label = (key: string, value: string) => fg("muted", key.padEnd(9) + " ") + value;
69
70
  const wrap = (lines: string[], width: number) => lines.flatMap(line => wrapTextWithAnsi(line, Math.max(1, width)));
70
71
  const editor = new Editor(tui, {
@@ -159,7 +160,8 @@ export async function showTeamsPanel(ctx: ExtensionContext, host: PanelHost): Pr
159
160
  else if (selected.right >= columnScroll + width) columnScroll = selected.right - width + 1;
160
161
  columnScroll = Math.max(0, Math.min(columnScroll, Math.max(0, art.width - width)));
161
162
  const rows = art.styled.map((spans, y) => {
162
- let row = spans.map(span => fg(span.cls === "border" || span.cls === "edge" ? "dim" : "text", span.text)).join("");
163
+ const row = spans.map(span => fg(span.cls === "border" || span.cls === "edge" ? "dim" : "text", span.text)).join("");
164
+ const replacements: { left: number; right: number; text: string }[] = [];
163
165
  for (const task of tasks) {
164
166
  const box = bounds.get(task.id);
165
167
  if (!box || y < box.top || y > box.bottom) continue;
@@ -174,10 +176,18 @@ export async function showTeamsPanel(ctx: ExtensionContext, host: PanelHost): Pr
174
176
  // The selected box keeps its status colour but gets a heavy border and bold text, so selection never competes with status.
175
177
  const edge = (part: string) => statusText(task, active ? part.replace(/[┌┐└┘─│┬┴]/g, ch => ({ "┌": "┏", "┐": "┓", "└": "┗", "┘": "┛", "─": "━", "│": "┃", "┬": "┯", "┴": "┷" })[ch]!) : part);
176
178
  const styled = border ? edge(text) : edge("│") + (active ? theme.bold(interior) : interior) + edge("│");
177
- row = sliceByColumn(row, 0, box.left) + styled + sliceByColumn(row, box.right + 1, art.width);
179
+ replacements.push({ left: box.left, right: box.right, text: styled });
178
180
  }
181
+ // Slice the original row once per gap; re-slicing styled replacements multiplies ANSI codes.
182
+ let column = 0;
183
+ const parts = replacements.sort((a, b) => a.left - b.left).flatMap(box => {
184
+ const gap = sliceByColumn(row, column, box.left - column);
185
+ column = box.right + 1;
186
+ return [gap, box.text];
187
+ });
188
+ parts.push(sliceByColumn(row, column, art.width - column));
179
189
  const indent = " ".repeat(Math.max(0, Math.floor((width - art.width) / 2)));
180
- return indent + sliceByColumn(row, columnScroll, width);
190
+ return indent + sliceByColumn(parts.join(""), columnScroll, width);
181
191
  });
182
192
  return { rows, selectedRow: selected.top, selectedHeight: selected.bottom - selected.top + 1, width: art.width };
183
193
  }
@@ -204,11 +214,13 @@ export async function showTeamsPanel(ctx: ExtensionContext, host: PanelHost): Pr
204
214
  let selectedHeight = 1, graphWidth = 0;
205
215
  if (tab === 0) {
206
216
  const graphData = graph ? graphLines(leftWidth) : undefined;
207
- left = graphData ? graphData.rows : tasks.length ? [fg("muted", " Task"), "",
217
+ const titleWidth = Math.max(1, leftWidth - 12);
218
+ left = graphData ? graphData.rows : tasks.length ? [tableHeader(` ${pad("Task", titleWidth)} State`), "",
208
219
  ...tasks.map((task, index) => {
209
- const text = `${index === taskIndex ? "●" : " "} ${taskTitle(task.id)}`;
210
- const body = fg(taskStatus(task) === "Done" ? "dim" : "text", truncateToWidth(text, Math.max(1, leftWidth - 10)));
211
- return (index === taskIndex ? theme.bold(body) : body) + " · " + stateLabel(task);
220
+ const selected = index === taskIndex;
221
+ const body = fg(taskStatus(task) === "Done" ? "dim" : "text", pad(taskTitle(task.id), titleWidth));
222
+ return fg(selected ? "accent" : "dim", `${selected ? "●" : " "} `)
223
+ + (selected ? theme.bold(body) : body) + " " + stateLabel(task);
212
224
  })] : [heading("No tasks yet"), "N Request a task from Main"];
213
225
  selectedRow = graphData ? graphData.selectedRow : taskIndex + 2;
214
226
  selectedHeight = graphData?.selectedHeight ?? 1;
@@ -231,7 +243,7 @@ export async function showTeamsPanel(ctx: ExtensionContext, host: PanelHost): Pr
231
243
  const nameExtra = Math.min(spare, Math.max(0, ...ids.map(id => visibleWidth(name(id)) - nameWidth)));
232
244
  const gap = " ".repeat(4 + Math.floor((spare - nameExtra) / gaps));
233
245
  nameWidth += nameExtra + (spare - nameExtra) % gaps;
234
- left = [fg("muted", ` ${pad("Member", nameWidth)}${gap}${pad("State", sessionWidth)}`
246
+ left = [tableHeader(` ${pad("Member", nameWidth)}${gap}${pad("State", sessionWidth)}`
235
247
  + (showTasks ? `${gap}${pad("Done", tasksWidth)}` : "") + (showDirectory ? `${gap}Directory` : "")), ...ids.map((id, index) => {
236
248
  const member = snapshot.members.find(member => member.id === id);
237
249
  const selected = index === memberIndex;
@@ -254,7 +266,7 @@ export async function showTeamsPanel(ctx: ExtensionContext, host: PanelHost): Pr
254
266
  const fromWidth = Math.min(16, Math.max(4, ...events.map(event => visibleWidth(name(event.from)))));
255
267
  const taskWidth = Math.min(5, Math.max(4, ...events.map(event => event.task ? visibleWidth(taskRef(event.task)) : 0)));
256
268
  const summaryWidth = Math.max(1, leftWidth - (2 + fromWidth + 2 + 7 + 2 + 2 + taskWidth + 2 + 5));
257
- left = events.length ? [fg("muted", ` ${pad("From", fromWidth)} ${pad("Kind", 7)} ${pad("Summary", summaryWidth)} ${pad("Task", taskWidth)} Time`),
269
+ left = events.length ? [tableHeader(` ${pad("From", fromWidth)} ${pad("Kind", 7)} ${pad("Summary", summaryWidth)} ${pad("Task", taskWidth)} Time`),
258
270
  ...events.map((event, index) => {
259
271
  const selected = index === eventIndex;
260
272
  const from = pad(name(event.from), fromWidth), summary = pad(headline(event), summaryWidth);
package/src/render.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { stripVTControlCharacters } from "node:util";
2
2
  import { keyHint, type MessageRenderer, type Theme, type ToolRenderers } from "@earendil-works/pi-coding-agent";
3
- import { Container, Text, TruncatedText, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
3
+ import { Box, Container, Text, TruncatedText, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
4
4
 
5
5
  export const cleanText = (text: string): string => stripVTControlCharacters(text).replaceAll("\t", " ").replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, "");
6
6
  const inline = (value: unknown) => cleanText(String(value)).replace(/[\n\r\t]/g, " ");
@@ -125,7 +125,7 @@ export function teamToolRenderers(name: string, getLabels: MemberLabels = defaul
125
125
  }
126
126
 
127
127
  export function teamMessageRenderer(getLabels: MemberLabels = defaultLabels): MessageRenderer {
128
- return (message, { expanded }, theme) => {
128
+ return (message, { expanded, outputPad }, theme) => {
129
129
  const details = message.details as { through?: number; messages?: { from: string; summary: string; recipients: string[]; task?: string; kind?: string }[] } | undefined;
130
130
  const posts = details?.messages ?? [], recent = posts.at(-1);
131
131
  const more = posts.length > 1 ? ` (+${posts.length - 1} more)` : "";
@@ -134,7 +134,9 @@ export function teamMessageRenderer(getLabels: MemberLabels = defaultLabels): Me
134
134
  // Lead with what happened: "count-a completed #1 Count lines · 37 lines", or "count-a → Main · #1 … · question".
135
135
  const what = recent && (recent.kind === "outcome" && recent.task ? `${name(recent.from)} completed ${name(recent.task)}` : `${name(recent.from)} → ${recent.recipients.map(name).join(", ")}${recent.task ? ` · ${name(recent.task)}` : ""}`);
136
136
  const preview = recent ? `${what} · ${recent.summary.split("\n")[0]}${more}` : `Updates through ${details?.through ?? "unknown"}`;
137
- return entry("ac-teams", details ?? { content: message.content }, expanded, theme, false, preview);
137
+ const box = new Box(outputPad, 0);
138
+ box.addChild(entry("ac-teams", details ?? { content: message.content }, expanded, theme, false, preview));
139
+ return box;
138
140
  };
139
141
  }
140
142