@aemonge-dev/pi-stardeck 0.6.0 → 0.7.1

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": "@aemonge-dev/pi-stardeck",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "A Vim- and tmux-inspired command deck for Pi",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -386,21 +386,25 @@ function globalLeft(state: HeaderDeckState, theme: Theme): string {
386
386
  }
387
387
 
388
388
  function projectDeckLeft(state: HeaderDeckState, theme: Theme, width: number): string {
389
- // Line 1 left: the devbox marker is a path prefix, then the path; the git
390
- // segment yields below 79 columns before the path does. The companion
391
- // chips ride line 1's right end and never drop (always-visible contract).
392
- const devbox = state.devbox ? `${color(theme, 'success', '[󰆧]', true)} ` : '';
393
- const path = `${devbox}${color(theme, 'success', ` ${safeText(state.cwd)}`)}`;
389
+ // Line 1 left: the path; the git segment yields below 79 columns before
390
+ // the path does. The devbox marker moved to the line-1 corner with the
391
+ // companion chips (deck-machine-corner); the identity cluster never
392
+ // drops, so the path never carries it again.
393
+ const path = color(theme, 'success', ` \u{F07B} ${safeText(state.cwd)}`);
394
394
  const gitSegment =
395
395
  width >= 79 ? truncateToWidth(gitRight(state, theme), 52, '…') : '';
396
396
  return gitSegment ? `${path} ${semanticSeparator(theme)} ${gitSegment}` : path;
397
397
  }
398
398
 
399
- const KNOWN_COMPANION_ICONS: ReadonlyArray<{
399
+ type CompanionSpec = {
400
400
  keys: readonly string[];
401
401
  icons: readonly string[];
402
- }> = [
403
- { keys: ['engram'], icons: ['\u{21BB}', '\u{25A4}'] },
402
+ /** Render the chip bracketed even at one icon (engram identity). */
403
+ bracket?: boolean;
404
+ };
405
+
406
+ const KNOWN_COMPANION_ICONS: ReadonlyArray<CompanionSpec> = [
407
+ { keys: ['engram'], icons: ['\u{21BB}'], bracket: true },
404
408
  { keys: ['graphify'], icons: ['\u{232C}'] },
405
409
  { keys: ['appa', 'openappa'], icons: ['\u{26E8}'] },
406
410
  ];
@@ -417,22 +421,37 @@ function companionIconColor(text: string | undefined, active: boolean): string {
417
421
  : 'accent';
418
422
  }
419
423
 
420
- function extensionStatusCodes(state: HeaderDeckState, theme: Theme): string {
424
+ function identityCluster(
425
+ state: HeaderDeckState,
426
+ theme: Theme,
427
+ ): { text: string; trailingBare: boolean } {
428
+ // Line-1 corner identity: devbox lead chip, then the companion chips.
429
+ // Returns the assembled text plus whether the last chip is bare — bare
430
+ // chips keep the wide gap to their right (0.7.1 amendment: the major
431
+ // separator rides double-spaced after a bare trailing chip, matching
432
+ // the bare→bare chip spacing).
421
433
  const texts = new Map<string, string>();
422
434
  for (const item of state.piStatus?.nativeStatuses ?? []) {
423
435
  if (item.key === 'goal' || texts.has(item.key)) continue;
424
436
  texts.set(item.key, item.text);
425
437
  }
426
438
  const parts: Array<{ bare: boolean; text: string }> = [];
439
+ if (state.devbox) {
440
+ // Corner lead chip (deck-machine-corner): the devbox marker joins
441
+ // the identity cluster ahead of the companions, success-colored as
442
+ // it was as a path prefix.
443
+ parts.push({ bare: false, text: color(theme, 'success', '[\u{F01A7}]', true) });
444
+ }
427
445
  for (const companion of KNOWN_COMPANION_ICONS) {
428
446
  const published = companion.keys
429
447
  .map((key) => texts.get(key))
430
448
  .find((text) => text !== undefined);
431
449
  for (const key of companion.keys) texts.delete(key);
432
450
  const joined = companion.icons.join(' ');
433
- const chip = companion.icons.length > 1 ? `[${joined}]` : joined;
451
+ const bracketed = companion.bracket === true || companion.icons.length > 1;
452
+ const chip = bracketed ? `[${joined}]` : joined;
434
453
  parts.push({
435
- bare: companion.icons.length === 1,
454
+ bare: !bracketed,
436
455
  text: color(
437
456
  theme,
438
457
  companionIconColor(published, published !== undefined),
@@ -441,14 +460,14 @@ function extensionStatusCodes(state: HeaderDeckState, theme: Theme): string {
441
460
  ),
442
461
  });
443
462
  }
444
- let out = '';
445
- let previousBare = false;
463
+ let text = '';
464
+ let trailingBare = false;
446
465
  for (const part of parts) {
447
- out +=
448
- out === '' ? part.text : `${previousBare && part.bare ? ' ' : ' '}${part.text}`;
449
- previousBare = part.bare;
466
+ text +=
467
+ text === '' ? part.text : `${trailingBare && part.bare ? ' ' : ' '}${part.text}`;
468
+ trailingBare = part.bare;
450
469
  }
451
- return out;
470
+ return { text, trailingBare };
452
471
  }
453
472
 
454
473
  const BLOCKED_NOTICE = '\u26d4 blocked';
@@ -516,37 +535,68 @@ function telemetryRowLeft(state: HeaderDeckState, theme: Theme): string {
516
535
  return modelGroupLeft(state, theme);
517
536
  }
518
537
 
519
- function telemetryRowRight(
538
+ export function machineCornerSegments(
520
539
  state: HeaderDeckState,
521
540
  theme: Theme,
522
541
  nowMs = Date.now(),
523
- ): string {
524
- // Line 4 right (2.4 second amendment): the machine group — the coding
525
- // quota flame with its countdown, machine readings, global cost — in
526
- // that order, anchoring the row's right end.
527
- const machineSegments: string[] = [];
542
+ ): string[] {
543
+ // Machine corner (deck-machine-corner): icon-led machine readings for
544
+ // the line-1 corner — coding quota flame with its countdown, CPU,
545
+ // memory, MCP availability, global cost — in that frozen order, joined
546
+ // by minor separators. Segments yield right-to-left on narrow frames.
547
+ const segments: string[] = [];
548
+ const flame = windowQuotaTiles(state, theme, nowMs);
549
+ if (flame !== '') segments.push(flame);
528
550
  const resources = state.resources;
529
551
  if (resources) {
530
552
  const resourceColor =
531
553
  resources.cpuWarning || resources.memoryWarning ? 'warning' : 'accent';
532
- const parts = [
533
- color(theme, resourceColor, `${formatDeckCpu(resources.cpuPercent)}`),
534
- color(theme, resourceColor, `${formatDeckBytes(resources.memoryBytes)}`),
535
- ];
536
- machineSegments.push(parts.join(` ${minorSeparator(theme, resourceColor)} `));
554
+ segments.push(
555
+ color(theme, resourceColor, `\u{F035B} ${formatDeckCpu(resources.cpuPercent)}`),
556
+ );
557
+ segments.push(
558
+ color(
559
+ theme,
560
+ resourceColor,
561
+ `\u{F061A} ${formatDeckBytes(resources.memoryBytes)}`,
562
+ ),
563
+ );
537
564
  }
538
565
  if (state.mcp) {
539
- machineSegments.push(
540
- color(theme, 'accent', `${state.mcp.healthy}/${state.mcp.total}`),
541
- );
566
+ segments.push(color(theme, 'accent', `${state.mcp.healthy}/${state.mcp.total}`));
567
+ }
568
+ const cost = costTile(state, theme);
569
+ if (cost !== '') segments.push(cost);
570
+ return segments;
571
+ }
572
+
573
+ function deckCornerRight(
574
+ state: HeaderDeckState,
575
+ theme: Theme,
576
+ width: number,
577
+ nowMs = Date.now(),
578
+ ): string {
579
+ // Line 1 corner: the identity cluster (devbox lead, companion chips)
580
+ // split from the machine group by one major separator. A bare trailing
581
+ // chip keeps the wide gap before the separator (0.7.1 amendment);
582
+ // machine segments yield right-to-left (cost first) before the path
583
+ // truncates below a 20-column floor; the identity cluster never drops
584
+ // (always-visible contract).
585
+ const identity = identityCluster(state, theme);
586
+ const segments = machineCornerSegments(state, theme, nowMs);
587
+ const pathFloor = 20;
588
+ const cornerGap = identity.trailingBare ? ' ' : ' ';
589
+ for (let keep = segments.length; keep >= 0; keep -= 1) {
590
+ const tail = segments.slice(0, keep).join(` ${minorSeparator(theme)} `);
591
+ const corner =
592
+ tail === ''
593
+ ? identity.text
594
+ : `${identity.text}${cornerGap}${semanticSeparator(theme)} ${tail}`;
595
+ if (keep === 0 || width - visibleWidth(corner) - 1 >= pathFloor) {
596
+ return corner;
597
+ }
542
598
  }
543
- return [
544
- windowQuotaTiles(state, theme, nowMs),
545
- machineSegments.join(` ${semanticSeparator(theme)} `),
546
- costTile(state, theme),
547
- ]
548
- .filter((group) => group !== '')
549
- .join(` ${semanticSeparator(theme)} `);
599
+ return identity.text;
550
600
  }
551
601
 
552
602
  function modelGroupLeft(state: HeaderDeckState, theme: Theme): string {
@@ -760,17 +810,17 @@ export function renderHeaderDeck(
760
810
  turnCostTile(state, theme),
761
811
  boundedWidth,
762
812
  );
763
- // Line 4 (2.4 second amendment + Task 3 placement default): the engine
764
- // group holds the row's left end. Its right end belongs to the machine
765
- // group — until notices land: a non-empty notice slot owns the right end
766
- // and the machine group yields wholesale, riding the same ladder as the
767
- // narrow-frame yield (the right group renders only when it fits beside
768
- // the engine group at full width; the engine group never yields to it).
813
+ // Line 4 (deck-machine-corner amendment): the engine group holds the
814
+ // row's left end; the right end is notice-only. The machine group moved
815
+ // to the line-1 corner, so notices can no longer starve machine
816
+ // readings — the old wholesale yield is gone with the group itself.
817
+ // 0.7.1 amendment: a clean notice slot renders the dim `healthy`
818
+ // placeholder instead of trailing silence — it yields like a notice
819
+ // when the engine group needs the width.
769
820
  const engineGroup = telemetryRowLeft(state, theme);
770
821
  const noticeGroup = noticeTile(state, theme);
771
- const rightGroup = noticeGroup !== '' ? noticeGroup : telemetryRowRight(state, theme);
822
+ const rightGroup = noticeGroup !== '' ? noticeGroup : color(theme, 'dim', 'healthy');
772
823
  const telemetryRow =
773
- rightGroup !== '' &&
774
824
  visibleWidth(engineGroup) + 1 + visibleWidth(rightGroup) <= boundedWidth
775
825
  ? alignedSingleRow(engineGroup, rightGroup, boundedWidth)
776
826
  : fit(engineGroup, boundedWidth);
@@ -781,17 +831,17 @@ export function renderHeaderDeck(
781
831
  // line 4; pi's own prompt chrome below closes the visual block — while
782
832
  // the internal seams stay dotted ┈ between lines 2 and 3 and between
783
833
  // line 3 (spans rows included) and line 4.
784
- const chips = extensionStatusCodes(state, theme);
834
+ const corner = deckCornerRight(state, theme, boundedWidth);
785
835
  const alert = statusAlertTile(
786
836
  state,
787
- Math.max(0, boundedWidth - visibleWidth(chips) - 2),
837
+ Math.max(0, boundedWidth - visibleWidth(corner) - 2),
788
838
  theme,
789
839
  lineColor,
790
840
  );
791
841
  const rows = [
792
842
  alignedSingleRow(
793
843
  projectDeckLeft(state, theme, boundedWidth),
794
- alert ? `${alert} ${chips}` : chips,
844
+ alert ? `${alert} ${corner}` : corner,
795
845
  boundedWidth,
796
846
  ),
797
847
  solidRule,