@crab-dev/wake 0.1.23 → 0.1.24

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.24
4
+
5
+ - Added Activity, Recent Problems, and Changes views to the interactive `wake dev` and `wake docs dev`
6
+ dashboards, with keyboard view switching, independent scrolling, unread counters, explicit empty
7
+ states, failed workspace names, and richer rebuild batches.
8
+ - Preserved structured diagnostics in both Rust and npm terminal implementations so errors include
9
+ severity, codes, locations, source snippets, and notes; warnings no longer incorrectly switch the
10
+ dashboard to `ERROR`.
11
+ - Added a shared terminal dashboard contract fixture and synchronized Rust/npm coverage for severity,
12
+ workspace failures, Unicode and multiline diagnostics, view navigation, clearing, and small-terminal
13
+ rendering.
14
+
3
15
  ## 0.1.23
4
16
 
5
17
  - Made Yarn 4.16 PnP authoritative for package visibility, alias dependencies, peer/virtual packages,
package/bin/console.mjs CHANGED
@@ -186,7 +186,7 @@ const KEY_SEQUENCES = new Map([
186
186
  ['\x1b[H', 'home'], ['\x1b[F', 'end'], ['\x1b[1~', 'home'], ['\x1b[4~', 'end'],
187
187
  ['\x1bOH', 'home'], ['\x1bOF', 'end'],
188
188
  ['\x1b[3~', 'delete'], ['\x1b[5~', 'pageup'], ['\x1b[6~', 'pagedown'],
189
- ['\x1b[1;5F', 'ctrl-end'],
189
+ ['\x1b[1;5F', 'ctrl-end'], ['\x1b[Z', 'shift-tab'],
190
190
  ])
191
191
 
192
192
  export class TerminalInputDecoder {
@@ -238,7 +238,7 @@ export class TerminalInputDecoder {
238
238
  this.buffer = this.buffer.slice(value.length)
239
239
  const control = {
240
240
  '\u0003': 'ctrl-c', '\u0019': 'ctrl-y', '\u0016': 'ctrl-v', '\r': 'enter', '\n': 'enter',
241
- '\u007f': 'backspace', '\u0008': 'backspace', '\u001b': 'escape',
241
+ '\u0009': 'tab', '\u007f': 'backspace', '\u0008': 'backspace', '\u001b': 'escape',
242
242
  }[value]
243
243
  events.push(control ? { type: 'key', key: control } : { type: 'text', value })
244
244
  }
package/bin/terminal.mjs CHANGED
@@ -12,6 +12,9 @@ const RESET = '\x1b[0m'
12
12
  const BOLD = '\x1b[1m'
13
13
  const DIM = '\x1b[2m'
14
14
  const MAX_ACTIVITY = 200
15
+ const MAX_PROBLEMS = 100
16
+ const MAX_CHANGES = 50
17
+ const VIEWS = ['activity', 'problems', 'changes']
15
18
  const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧']
16
19
  const OPTIONAL_CLIPBOARD_MODULE = 'clip' + 'boardy'
17
20
  const OPTIONAL_OPEN_MODULE = 'op' + 'en'
@@ -258,12 +261,93 @@ export function observeServer(server, ui, output = console) {
258
261
  }
259
262
 
260
263
  function pushActivity(state, level, message) {
264
+ const before = activityRowCount(state)
261
265
  if (state.activity.length === MAX_ACTIVITY) state.activity.shift()
262
266
  state.activity.push({
263
267
  elapsedMs: Date.now() - state.startedAt,
264
268
  level,
265
269
  message: String(message),
266
270
  })
271
+ noteViewUpdate(state, 'activity', before, activityRowCount(state))
272
+ }
273
+
274
+ function diagnosticLevel(severity) {
275
+ if (String(severity).toLowerCase() === 'error') return 'error'
276
+ if (String(severity).toLowerCase() === 'warning') return 'warning'
277
+ return 'info'
278
+ }
279
+
280
+ function pushProblem(state, diagnostic, rendered) {
281
+ const before = problemRowCount(state)
282
+ if (state.problems.length === MAX_PROBLEMS) state.problems.shift()
283
+ state.problems.push({
284
+ elapsedMs: Date.now() - state.startedAt,
285
+ diagnostic,
286
+ rendered,
287
+ })
288
+ noteViewUpdate(state, 'problems', before, problemRowCount(state))
289
+ }
290
+
291
+ function pushChange(state, event) {
292
+ const before = changeRowCount(state)
293
+ if (state.changes.length === MAX_CHANGES) state.changes.shift()
294
+ state.changes.push({
295
+ elapsedMs: Date.now() - state.startedAt,
296
+ changedPaths: [...(event.changedPaths || [])],
297
+ workspace: event.workspace,
298
+ basePath: event.basePath,
299
+ metrics: undefined,
300
+ })
301
+ noteViewUpdate(state, 'changes', before, changeRowCount(state))
302
+ }
303
+
304
+ function completeChange(state, event) {
305
+ if (event.initial) return
306
+ const before = changeRowCount(state)
307
+ let change
308
+ for (let index = state.changes.length - 1; index >= 0; index -= 1) {
309
+ const candidate = state.changes[index]
310
+ if (!candidate.metrics
311
+ && candidate.workspace === event.workspace
312
+ && candidate.basePath === event.basePath) {
313
+ change = candidate
314
+ break
315
+ }
316
+ }
317
+ const metrics = {
318
+ modules: event.modules,
319
+ updatedModules: event.updatedModules,
320
+ cachedModules: event.cachedModules,
321
+ chunks: event.chunks,
322
+ assets: event.assets,
323
+ durationMs: event.durationMs,
324
+ }
325
+ if (change) change.metrics = metrics
326
+ else {
327
+ if (state.changes.length === MAX_CHANGES) state.changes.shift()
328
+ state.changes.push({
329
+ elapsedMs: Date.now() - state.startedAt,
330
+ changedPaths: [],
331
+ workspace: event.workspace,
332
+ basePath: event.basePath,
333
+ metrics,
334
+ })
335
+ }
336
+ noteViewUpdate(state, 'changes', before, changeRowCount(state))
337
+ }
338
+
339
+ function noteViewUpdate(state, view, before, after) {
340
+ const scroll = state.scroll[view]
341
+ if (scroll.fromBottom === 0) return
342
+ scroll.fromBottom = Math.max(0, scroll.fromBottom + after - before)
343
+ scroll.unread += 1
344
+ }
345
+
346
+ function clearDashboardHistory(state) {
347
+ state.activity.length = 0
348
+ state.problems.length = 0
349
+ state.changes.length = 0
350
+ for (const view of VIEWS) state.scroll[view] = { fromBottom: 0, unread: 0 }
267
351
  }
268
352
 
269
353
  export function createDashboardState({
@@ -283,8 +367,11 @@ export function createDashboardState({
283
367
  rebuilds: 0,
284
368
  startedAt: Date.now(),
285
369
  activity: [],
370
+ problems: [],
371
+ changes: [],
286
372
  workspaceState: undefined,
287
- scrollFromBottom: 0,
373
+ view: 'activity',
374
+ scroll: Object.fromEntries(VIEWS.map((view) => [view, { fromBottom: 0, unread: 0 }])),
288
375
  }
289
376
  pushActivity(state, 'info', 'Starting Wake…')
290
377
  return state
@@ -294,6 +381,7 @@ export function applyDashboardEvent(state, event) {
294
381
  if (event.type === 'rebuildStart') {
295
382
  const count = event.changedPaths?.length || 0
296
383
  state.status = 'rebuilding'
384
+ pushChange(state, event)
297
385
  pushActivity(
298
386
  state,
299
387
  'warning',
@@ -314,6 +402,7 @@ export function applyDashboardEvent(state, event) {
314
402
  durationMs: event.durationMs,
315
403
  }
316
404
  if (!event.initial) state.rebuilds += 1
405
+ completeChange(state, event)
317
406
  pushActivity(
318
407
  state,
319
408
  'success',
@@ -322,8 +411,11 @@ export function applyDashboardEvent(state, event) {
322
411
  : `Updated ${moduleCount(event.updatedModules)} · ${cacheHitCount(event.cachedModules)} in ${humanDuration(event.durationMs)}`,
323
412
  )
324
413
  } else if (event.type === 'diagnostic') {
325
- state.status = 'error'
326
- pushActivity(state, 'error', formatDiagnostic(createUi(false), event.diagnostic).join('\n'))
414
+ const level = diagnosticLevel(event.diagnostic?.severity)
415
+ if (level === 'error') state.status = 'error'
416
+ const rendered = formatDiagnostic(createUi(false), event.diagnostic).join('\n')
417
+ pushProblem(state, event.diagnostic, rendered)
418
+ pushActivity(state, level, rendered)
327
419
  } else if (event.type === 'workspaceState') {
328
420
  state.workspaceState = {
329
421
  total: event.total,
@@ -428,14 +520,56 @@ function metricsText(state) {
428
520
  return `BUILD ${metrics.modules} modules · ${metrics.chunks} chunks · ${metrics.assets} assets · ${humanDuration(metrics.durationMs)}`
429
521
  }
430
522
 
431
- function activityRows(state, available) {
432
- const rows = state.activity.flatMap((item) => {
433
- const symbol = { info: '·', success: '✓', warning: '↻', error: '✗' }[item.level]
434
- return String(item.message).split('\n').map((line, index) => index === 0
435
- ? `${elapsedStamp(item.elapsedMs)} ${symbol} ${line}`
436
- : ` ${line}`)
523
+ function eventRows(elapsedMs, level, message) {
524
+ const symbol = { info: '·', success: '✓', warning: '⚠', error: '✗' }[level]
525
+ return String(message).split('\n').map((line, index) => index === 0
526
+ ? `${elapsedStamp(elapsedMs)} ${symbol} ${line}`
527
+ : ` ${line}`)
528
+ }
529
+
530
+ function activityRows(state) {
531
+ return state.activity.flatMap((item) => eventRows(item.elapsedMs, item.level, item.message))
532
+ }
533
+
534
+ function problemRows(state) {
535
+ return state.problems.flatMap((problem) => eventRows(
536
+ problem.elapsedMs,
537
+ diagnosticLevel(problem.diagnostic?.severity),
538
+ problem.rendered,
539
+ ))
540
+ }
541
+
542
+ function changeRows(state) {
543
+ return state.changes.flatMap((change) => {
544
+ const target = change.workspace && change.basePath
545
+ ? `${change.workspace} · ${change.basePath}`
546
+ : change.workspace || change.basePath || 'project'
547
+ const rows = [`${elapsedStamp(change.elapsedMs)} ↻ ${target}`]
548
+ if (change.changedPaths.length === 0) rows.push(' Changed files unavailable')
549
+ else rows.push(...change.changedPaths.map((path) => ` ${path}`))
550
+ rows.push(change.metrics
551
+ ? ` ✓ ${change.metrics.updatedModules} updated · ${change.metrics.cachedModules} cache hits · ${humanDuration(change.metrics.durationMs)}`
552
+ : ' … rebuilding…')
553
+ return rows
437
554
  })
438
- const end = Math.max(0, rows.length - state.scrollFromBottom)
555
+ }
556
+
557
+ function emptyViewRows(view) {
558
+ if (view === 'problems') return ['No recent problems', 'Warnings and errors will appear here with source context.']
559
+ if (view === 'changes') return ['No rebuilds yet', 'Edit a file to trigger a rebuild.']
560
+ return ['No activity yet', 'Wake events will appear here.']
561
+ }
562
+
563
+ function allViewRows(state, view = state.view) {
564
+ if (view === 'problems') return problemRows(state)
565
+ if (view === 'changes') return changeRows(state)
566
+ return activityRows(state)
567
+ }
568
+
569
+ function visibleViewRows(state, available) {
570
+ let rows = allViewRows(state)
571
+ if (rows.length === 0) rows = emptyViewRows(state.view)
572
+ const end = Math.max(0, rows.length - state.scroll[state.view].fromBottom)
439
573
  const start = Math.max(0, end - Math.max(1, available))
440
574
  return rows.slice(start, end)
441
575
  }
@@ -444,7 +578,8 @@ function workspaceText(state) {
444
578
  const workspaces = state.workspaceState
445
579
  if (!workspaces) return undefined
446
580
  const current = workspaces.current ? ` · loading ${workspaces.current}` : ''
447
- const failed = workspaces.failed ? ` · ${workspaces.failed} failed` : ''
581
+ const failedNames = workspaces.failedNames?.length ? `: ${workspaces.failedNames.join(', ')}` : ''
582
+ const failed = workspaces.failed ? ` · ${workspaces.failed} failed${failedNames}` : ''
448
583
  return `WORKSPACES ${workspaces.loaded}/${workspaces.total} loaded${failed}${current}`
449
584
  }
450
585
 
@@ -452,6 +587,48 @@ function activityRowCount(state) {
452
587
  return state.activity.reduce((count, item) => count + String(item.message).split('\n').length, 0)
453
588
  }
454
589
 
590
+ function problemRowCount(state) {
591
+ return state.problems.reduce((count, problem) => count + String(problem.rendered).split('\n').length, 0)
592
+ }
593
+
594
+ function changeRowCount(state) {
595
+ return state.changes.reduce((count, change) => count + Math.max(1, change.changedPaths.length) + 2, 0)
596
+ }
597
+
598
+ function viewRowCount(state) {
599
+ if (state.view === 'problems') return problemRowCount(state)
600
+ if (state.view === 'changes') return changeRowCount(state)
601
+ return activityRowCount(state)
602
+ }
603
+
604
+ function viewTitle(state) {
605
+ return VIEWS.map((view) => {
606
+ const label = view === 'activity'
607
+ ? 'ACTIVITY'
608
+ : view === 'problems'
609
+ ? `RECENT PROBLEMS ${state.problems.length}`
610
+ : `CHANGES ${state.changes.length}`
611
+ const unread = state.scroll[view].unread ? ` +${state.scroll[view].unread}` : ''
612
+ return state.view === view ? `[${label}${unread}]` : `${label}${unread}`
613
+ }).join(' · ')
614
+ }
615
+
616
+ function cycleView(state, direction) {
617
+ const index = VIEWS.indexOf(state.view)
618
+ state.view = VIEWS[(index + direction + VIEWS.length) % VIEWS.length]
619
+ }
620
+
621
+ function scrollView(state, amount) {
622
+ const scroll = state.scroll[state.view]
623
+ const maximum = Math.max(0, viewRowCount(state) - 1)
624
+ scroll.fromBottom = Math.max(0, Math.min(maximum, scroll.fromBottom + amount))
625
+ if (scroll.fromBottom === 0) scroll.unread = 0
626
+ }
627
+
628
+ function scrollViewToBottom(state) {
629
+ state.scroll[state.view] = { fromBottom: 0, unread: 0 }
630
+ }
631
+
455
632
  function plainFrame(state, width, height, editor = new InputEditor(), notice) {
456
633
  width = Math.max(10, width || 80)
457
634
  height = Math.max(6, height || 24)
@@ -462,13 +639,13 @@ function plainFrame(state, width, height, editor = new InputEditor(), notice) {
462
639
  let activityHeight
463
640
 
464
641
  if (width < 60 || height < 14) {
465
- const latest = String(state.activity.at(-1)?.message || 'Starting Wake…').split('\n')
642
+ const latest = String(state.problems.at(-1)?.rendered || state.activity.at(-1)?.message || 'Starting Wake…').split('\n')
466
643
  fixed = [topBorder(state, width), boxLine(header, width)]
467
644
  const diagnosticRows = latest.length > 1
468
645
  for (const line of latest.slice(0, Math.max(1, height - (diagnosticRows ? 5 : 6)))) {
469
646
  fixed.push(boxLine(line, width))
470
647
  }
471
- if (!diagnosticRows) fixed.push(boxLine('Resize for details · type help for commands', width))
648
+ if (!diagnosticRows) fixed.push(boxLine('Resize for views · type help for commands', width))
472
649
  activityHeight = 0
473
650
  } else if (width < 80 || height < 20) {
474
651
  fixed = [
@@ -476,7 +653,7 @@ function plainFrame(state, width, height, editor = new InputEditor(), notice) {
476
653
  boxLine(header, width),
477
654
  boxLine(`${state.endpointLabel} ${endpoint}`, width),
478
655
  boxLine(metricsText(state), width),
479
- separator(width, 'ACTIVITY'),
656
+ separator(width, viewTitle(state)),
480
657
  ]
481
658
  if (workspaceText(state)) fixed.splice(-1, 0, boxLine(workspaceText(state), width))
482
659
  activityHeight = Math.max(1, height - fixed.length - 2)
@@ -490,20 +667,20 @@ function plainFrame(state, width, height, editor = new InputEditor(), notice) {
490
667
  boxLine(`MODE ${state.watchLabel}`, width),
491
668
  separator(width),
492
669
  boxLine(metricsText(state), width),
493
- separator(width, 'ACTIVITY'),
670
+ separator(width, viewTitle(state)),
494
671
  ]
495
672
  if (workspaceText(state)) fixed.splice(-1, 0, boxLine(workspaceText(state), width))
496
673
  activityHeight = Math.max(1, height - fixed.length - 2)
497
674
  }
498
675
 
499
- for (const row of activityRows(state, activityHeight)) fixed.push(boxLine(row, width))
676
+ for (const row of visibleViewRows(state, activityHeight)) fixed.push(boxLine(row, width))
500
677
  while (fixed.length < height - 3) fixed.push(boxLine('', width))
501
678
  fixed.splice(Math.max(0, height - 3))
502
679
  const visible = editor.visible(Math.max(0, width - 6))
503
680
  fixed.push(boxLine(
504
681
  notice
505
682
  ? `${notice.error ? '✗' : '✓'} ${notice.message}`
506
- : 'Enter command · PgUp/PgDn logs · drag to copy · Ctrl-C quit',
683
+ : 'Tab views · PgUp/PgDn scroll · Enter command · drag copy · Ctrl-C quit',
507
684
  width,
508
685
  ))
509
686
  fixed.push(boxLine(`› ${visible.text}`, width))
@@ -517,7 +694,7 @@ function colorizeLine(line, ui) {
517
694
  value = value.replace('WAKE', ui.brand('WAKE'))
518
695
  value = value.replace(/(✓|■)/, (match) => ui.ok(match))
519
696
  value = value.replace(/(✗)/, (match) => ui.error(match))
520
- value = value.replace(/(↻|⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧|◌)/, (match) => ui.warn(match))
697
+ value = value.replace(/(⚠|↻|⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧|◌)/, (match) => ui.warn(match))
521
698
  value = value.replace(/(https?:\/\/\S+)/, (match) => ui.accent(match))
522
699
  return value
523
700
  }
@@ -640,9 +817,8 @@ export function createDashboardSession(
640
817
  if (result.error) pushActivity(state, 'warning', result.error)
641
818
  else if (result.command === 'help') pushActivity(state, 'info', 'Commands: help · clear · open · quit (a leading / is optional)')
642
819
  else if (result.command === 'clear') {
643
- state.activity.length = 0
644
- state.scrollFromBottom = 0
645
- setNotice('Activity cleared')
820
+ clearDashboardHistory(state)
821
+ setNotice('Dashboard history cleared')
646
822
  } else if (result.command === 'open') {
647
823
  if (!/^https?:\/\//u.test(state.endpoint)) pushActivity(state, 'warning', 'No development-server URL is available to open')
648
824
  else {
@@ -679,25 +855,27 @@ export function createDashboardSession(
679
855
  selection = undefined
680
856
  }
681
857
  } else if (event.kind === 'down' && event.button === 'right' && position.y === inputY) await pasteClipboard()
682
- else if (event.kind === 'scroll-up') state.scrollFromBottom = Math.max(0, Math.min(activityRowCount(state) - 1, state.scrollFromBottom + 3))
683
- else if (event.kind === 'scroll-down') state.scrollFromBottom = Math.max(0, state.scrollFromBottom - 3)
858
+ else if (event.kind === 'scroll-up') scrollView(state, 3)
859
+ else if (event.kind === 'scroll-down') scrollView(state, -3)
684
860
  } else if (event.type === 'key') {
685
861
  if (event.key === 'ctrl-c') requestExit('SIGINT')
686
862
  else if (event.key === 'ctrl-y') lastSelection ? await copyText(lastSelection) : setNotice('No selected text to copy', true)
687
863
  else if (event.key === 'ctrl-v') await pasteClipboard()
688
864
  else if (event.key === 'enter') await submit()
865
+ else if (event.key === 'tab') { cycleView(state, 1); selection = undefined }
866
+ else if (event.key === 'shift-tab') { cycleView(state, -1); selection = undefined }
689
867
  else if (event.key === 'escape') { editor.clear(); selection = undefined }
690
868
  else if (event.key === 'left') editor.moveLeft()
691
869
  else if (event.key === 'right') editor.moveRight()
692
870
  else if (event.key === 'home') editor.moveHome()
693
871
  else if (event.key === 'end') editor.moveEnd()
694
- else if (event.key === 'ctrl-end') state.scrollFromBottom = 0
872
+ else if (event.key === 'ctrl-end') scrollViewToBottom(state)
695
873
  else if (event.key === 'backspace') editor.backspace()
696
874
  else if (event.key === 'delete') editor.delete()
697
875
  else if (event.key === 'up') editor.historyPrevious()
698
876
  else if (event.key === 'down') editor.historyNext()
699
- else if (event.key === 'pageup') state.scrollFromBottom = Math.max(0, Math.min(activityRowCount(state) - 1, state.scrollFromBottom + 10))
700
- else if (event.key === 'pagedown') state.scrollFromBottom = Math.max(0, state.scrollFromBottom - 10)
877
+ else if (event.key === 'pageup') scrollView(state, 10)
878
+ else if (event.key === 'pagedown') scrollView(state, -10)
701
879
  }
702
880
  draw()
703
881
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crab-dev/wake",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
4
4
  "description": "Wake native web build tools for Node.js",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "repository": {
@@ -64,7 +64,7 @@
64
64
  "node": ">=22.14 <27"
65
65
  },
66
66
  "dependencies": {
67
- "@crab-dev/css": "0.1.23",
67
+ "@crab-dev/css": "0.1.24",
68
68
  "@crab-dev/rc-alert": "^0.0.2",
69
69
  "@crab-dev/rc-button": "^0.0.2",
70
70
  "@crab-dev/rc-dialog": "^0.0.2",
@@ -95,11 +95,11 @@
95
95
  "react-dom": "19.2.8"
96
96
  },
97
97
  "optionalDependencies": {
98
- "@crab-dev/wake-darwin-arm64": "0.1.23",
99
- "@crab-dev/wake-darwin-x64": "0.1.23",
100
- "@crab-dev/wake-linux-arm64-gnu": "0.1.23",
101
- "@crab-dev/wake-linux-x64-gnu": "0.1.23",
102
- "@crab-dev/wake-win32-x64-msvc": "0.1.23"
98
+ "@crab-dev/wake-darwin-arm64": "0.1.24",
99
+ "@crab-dev/wake-darwin-x64": "0.1.24",
100
+ "@crab-dev/wake-linux-arm64-gnu": "0.1.24",
101
+ "@crab-dev/wake-linux-x64-gnu": "0.1.24",
102
+ "@crab-dev/wake-win32-x64-msvc": "0.1.24"
103
103
  },
104
104
  "publishConfig": {
105
105
  "access": "public",