@zgeoff/atc 0.1.8 → 0.1.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/src/ui.ts CHANGED
@@ -1,5 +1,8 @@
1
+ import type { AgentKind } from './agent-adapter';
1
2
  import { formatDir } from './dirs';
3
+ import { formatOverlayAgentMark } from './format-overlay-agent-mark';
2
4
  import { RESET_INPUT_MODES } from './reset-input-modes';
5
+ import { PINNED_GROUP_KEY } from './sessions';
3
6
  import type { SessionState } from './sessions';
4
7
 
5
8
  // The slice of a session the drawing layer needs; satisfied by both the
@@ -158,9 +161,11 @@ function dimRow(width: number, text: string): Row {
158
161
  // Selection-dependent hints need the liveness facts of each row.
159
162
  export interface OverlaySessionView extends SessionView {
160
163
  readonly alive: boolean;
161
- readonly kind: 'pty' | 'jsonl';
164
+ readonly kind: 'pty' | 'headless';
162
165
  readonly resumable: boolean;
163
- readonly group?: string;
166
+ readonly agent: AgentKind;
167
+ readonly pinned: boolean;
168
+ readonly repoRoot: string;
164
169
  }
165
170
 
166
171
  export interface OverlayView {
@@ -169,6 +174,7 @@ export interface OverlayView {
169
174
  confirmKill: boolean;
170
175
  filter: string | null;
171
176
  stale: boolean;
177
+ grouped: boolean;
172
178
  }
173
179
 
174
180
  export function drawOverlay(view: OverlayView) {
@@ -181,32 +187,36 @@ export function drawOverlay(view: OverlayView) {
181
187
  rowsList.push(dimRow(width, empty));
182
188
  }
183
189
 
184
- // Sessions cluster under dim headers when more than one group exists: an
185
- // explicit group wins, sessions without one fall back to their spawn
186
- // directory. The rows keep the sorted order, so urgent groups lead.
187
- const grouped = new Set(view.sessions.map((s) => s.group ?? s.cwd)).size > 1;
190
+ // The grouped view clusters sessions under dim repository headers, with
191
+ // pinned sessions leading in their own cluster; the flat view shows a
192
+ // directory column instead. The rows keep the sorted order either way.
188
193
  let lastKey: string | null = null;
189
194
 
190
195
  for (const [i, s] of view.sessions.entries()) {
191
- const key = s.group ?? s.cwd;
196
+ const key = s.pinned ? PINNED_GROUP_KEY : s.repoRoot;
192
197
 
193
- if (grouped && key !== lastKey) {
198
+ if (view.grouped && key !== lastKey) {
194
199
  lastKey = key;
195
200
 
196
- rowsList.push(dimRow(width, `▸ ${s.group ?? formatDir(s.cwd)}`));
201
+ rowsList.push(dimRow(width, `▸ ${s.pinned ? 'pinned' : formatDir(s.repoRoot)}`));
197
202
  }
198
203
 
199
204
  const sel = i === view.selected;
200
205
  const name = truncate(s.name, 16).padEnd(16);
201
206
  const state = STATE_LABEL[s.state].padEnd(9);
202
- const dir = grouped ? '' : ` ${truncate(formatDir(s.cwd), 18).padEnd(18)}`;
203
- const msgWidth = Math.max(4, width - 4 - 2 - 17 - 10 - (grouped ? 0 : 19));
207
+ const dir = view.grouped ? '' : ` ${truncate(formatDir(s.cwd), 18).padEnd(18)}`;
208
+ const msgWidth = Math.max(4, width - 4 - 4 - 17 - 10 - (view.grouped ? 0 : 19));
204
209
  const msg = truncate(s.lastMsg, msgWidth).padEnd(msgWidth);
205
210
  const unread = s.unread ? `${ESC}[1;33m!${ESC}[0m` : ' ';
211
+ const pin = s.pinned ? `${ESC}[93m⋆${ESC}[0m` : ' ';
212
+ const mark = formatOverlayAgentMark(s.agent);
213
+ const styledMark = mark === 'g' ? `${ESC}[90m${mark}${ESC}[0m` : mark;
206
214
  const body = `${name} ${state}${dir} ${msg}`;
207
215
  const styledBody = sel ? `${ESC}[7m${body}${ESC}[0m` : body;
208
216
 
209
- rowsList.push(boxRow(width, `${GLYPH[s.state]}${unread}${styledBody}`, 2 + body.length));
217
+ rowsList.push(
218
+ boxRow(width, `${GLYPH[s.state]}${unread}${pin}${styledMark}${styledBody}`, 4 + body.length),
219
+ );
210
220
  }
211
221
 
212
222
  rowsList.push(boxDivider(width));
@@ -239,18 +249,18 @@ export function drawOverlay(view: OverlayView) {
239
249
  drawBox(rowsList);
240
250
  }
241
251
 
242
- const GLOBAL_HINT = 'n new · ? keys';
252
+ const GLOBAL_HINT = 'g groups · n new · ? keys';
243
253
 
244
254
  // Only the actions valid for the selected row appear; the full reference
245
- // lives behind ?.
246
- function buildOverlayHint(s: OverlaySessionView | undefined): string {
255
+ // lives behind ?. Grok has no headless handoff, so H is omitted there.
256
+ export function buildOverlayHint(s: OverlaySessionView | undefined): string {
247
257
  if (s === undefined) {
248
258
  return GLOBAL_HINT;
249
259
  }
250
260
 
251
261
  const actions: string[] = [];
252
262
 
253
- if (s.kind === 'jsonl' && s.alive) {
263
+ if (s.kind === 'headless' && s.alive) {
254
264
  actions.push('P reattach', 'K kill');
255
265
  } else if (s.alive) {
256
266
  actions.push('⏎ attach');
@@ -259,7 +269,11 @@ function buildOverlayHint(s: OverlaySessionView | undefined): string {
259
269
  actions.push('a ack');
260
270
  }
261
271
 
262
- actions.push('H headless', 'y yank', 'Y eject', 'K kill');
272
+ if (s.agent !== 'grok') {
273
+ actions.push('H headless');
274
+ }
275
+
276
+ actions.push('y yank', 'Y eject', 'K kill');
263
277
  } else {
264
278
  if (s.resumable) {
265
279
  actions.push('P revive', 'y yank');
@@ -268,6 +282,10 @@ function buildOverlayHint(s: OverlaySessionView | undefined): string {
268
282
  actions.push('K forget');
269
283
  }
270
284
 
285
+ const pinAction = s.pinned ? 'p unpin' : 'p pin';
286
+
287
+ actions.push(pinAction);
288
+
271
289
  return `${actions.join(' · ')} ▏ ${GLOBAL_HINT}`;
272
290
  }
273
291
 
@@ -278,11 +296,14 @@ export function drawHelp() {
278
296
  '⏎ attach the selected session',
279
297
  '⇥ attach the most urgent needs-you, else latest done',
280
298
  'a ack its notification without attaching',
281
- 'H eject to a headless run',
299
+ 'H eject to a headless run (Claude only)',
282
300
  'P revive a dead or headless session',
283
301
  'y yank the resume command to the clipboard',
284
302
  'Y yank the resume command, then kill here',
285
303
  'K kill (K again on a dead session forgets it)',
304
+ 'p pin or unpin — pinned sessions stay on top',
305
+ 'g toggle grouping by repository',
306
+ ' Grok rows show a dim g after the pin mark',
286
307
  'n new session',
287
308
  'r adopt an external session',
288
309
  '/ filter · ↑↓/jk move · q quit',
@@ -350,10 +371,10 @@ export function drawHome(fleetCount = 0, leaderLabel = '^Space') {
350
371
  out(ansi.clear + ansi.hideCursor);
351
372
 
352
373
  const msgs = [
353
- 'atc — control tower for claude sessions',
374
+ 'atc — control tower for Claude and Grok sessions',
354
375
  '',
355
376
  'n spawn a session',
356
- 'r adopt an existing session (claude --resume)',
377
+ 'r adopt an existing session',
357
378
  ...(fleetCount > 0 ? [`R restore last fleet (${fleetCount} sessions)`] : []),
358
379
  `${leaderLabel.padEnd(7)} session list`,
359
380
  'q quit',