@nowline/mcp 0.8.6 → 0.9.0

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.
Files changed (58) hide show
  1. package/dist/capabilities.d.ts.map +1 -1
  2. package/dist/capabilities.js +2 -9
  3. package/dist/capabilities.js.map +1 -1
  4. package/dist/diagnostics.d.ts +43 -15
  5. package/dist/diagnostics.d.ts.map +1 -1
  6. package/dist/diagnostics.js +77 -25
  7. package/dist/diagnostics.js.map +1 -1
  8. package/dist/generated/resources.d.ts.map +1 -1
  9. package/dist/generated/resources.js +15 -3
  10. package/dist/generated/resources.js.map +1 -1
  11. package/dist/generated/ui-bundle.d.ts.map +1 -1
  12. package/dist/generated/ui-bundle.js +2 -2
  13. package/dist/generated/ui-bundle.js.map +1 -1
  14. package/dist/http.d.ts +4 -0
  15. package/dist/http.d.ts.map +1 -0
  16. package/dist/http.js +23 -0
  17. package/dist/http.js.map +1 -0
  18. package/dist/index.js +6 -12
  19. package/dist/index.js.map +1 -1
  20. package/dist/options.d.ts +26 -0
  21. package/dist/options.d.ts.map +1 -0
  22. package/dist/options.js +55 -0
  23. package/dist/options.js.map +1 -0
  24. package/dist/reference-cheatsheet.d.ts +1 -1
  25. package/dist/reference-cheatsheet.d.ts.map +1 -1
  26. package/dist/reference-cheatsheet.js +27 -2
  27. package/dist/reference-cheatsheet.js.map +1 -1
  28. package/dist/schema-vocab.d.ts +4 -2
  29. package/dist/schema-vocab.d.ts.map +1 -1
  30. package/dist/schema-vocab.js +9 -0
  31. package/dist/schema-vocab.js.map +1 -1
  32. package/dist/schemas.d.ts +2 -0
  33. package/dist/schemas.d.ts.map +1 -1
  34. package/dist/schemas.js +2 -0
  35. package/dist/schemas.js.map +1 -1
  36. package/dist/server.d.ts.map +1 -1
  37. package/dist/server.js +146 -56
  38. package/dist/server.js.map +1 -1
  39. package/dist/ui/entry.js +5 -12
  40. package/dist/ui/entry.js.map +1 -1
  41. package/dist/ui/payload.d.ts +14 -0
  42. package/dist/ui/payload.d.ts.map +1 -1
  43. package/dist/ui/payload.js +23 -0
  44. package/dist/ui/payload.js.map +1 -1
  45. package/package.json +21 -16
  46. package/src/capabilities.ts +4 -2
  47. package/src/diagnostics.ts +99 -39
  48. package/src/generated/resources.ts +15 -3
  49. package/src/generated/ui-bundle.ts +2 -2
  50. package/src/http.ts +28 -0
  51. package/src/index.ts +6 -13
  52. package/src/options.ts +79 -0
  53. package/src/reference-cheatsheet.ts +27 -2
  54. package/src/schema-vocab.ts +9 -0
  55. package/src/schemas.ts +2 -0
  56. package/src/server.ts +195 -57
  57. package/src/ui/entry.ts +6 -11
  58. package/src/ui/payload.ts +26 -0
package/src/server.ts CHANGED
@@ -15,14 +15,19 @@ import {
15
15
  registerAppTool,
16
16
  } from '@modelcontextprotocol/ext-apps/server';
17
17
  import { McpServer } from '@modelcontextprotocol/server';
18
+ import type { SurfaceOptions } from '@nowline/config';
18
19
  import { type NowlineFile, parseNowlineJson, printNowlineFile } from '@nowline/core';
19
20
  import {
21
+ acceptStagedRoadmap,
20
22
  type ExportFormat,
21
23
  exportDocument,
24
+ exportStaged,
22
25
  type HostEnv,
26
+ IncludeResolveError,
23
27
  type RenderInputs,
24
28
  } from '@nowline/export';
25
29
  import { resolveFonts } from '@nowline/export-core';
30
+ import { NON_WORKING_DISPLAYS, THEME_NAMES } from '@nowline/layout';
26
31
  import { buildShareLink } from '@nowline/share-link';
27
32
  import { z } from 'zod';
28
33
  import { NOWLINE_MCP_ICONS } from './branding.js';
@@ -30,14 +35,20 @@ import { CAPABILITIES } from './capabilities.js';
30
35
  import {
31
36
  buildDocument,
32
37
  collectMcpDiagnostics,
33
- collectMcpLayoutInsights,
34
38
  DEFAULT_RENDER_WIDTH,
35
39
  diagnosticsErrorBlock,
40
+ diagnosticsErrorResponse,
36
41
  handleToolError,
37
42
  InputRequiredError,
43
+ includeDiagnosticsResponse,
38
44
  LAYOUT_INSIGHT_HINT,
45
+ layoutInsightsFromStage,
46
+ type McpInsight,
39
47
  PathOutsideRootError,
40
48
  REVIEW_MAX_WIDTH,
49
+ routedResolveDiagnosticsToMcp,
50
+ stageMcpRoadmap,
51
+ stageRoutedDiagnostics,
41
52
  toolDescriptionWithSyntax,
42
53
  } from './diagnostics.js';
43
54
  import {
@@ -47,6 +58,7 @@ import {
47
58
  REFERENCE_MAN_PAGE,
48
59
  } from './generated/resources.js';
49
60
  import { PREVIEW_HTML } from './generated/ui-bundle.js';
61
+ import { type McpResolvedOptions, resolveMcpOptions } from './options.js';
50
62
  import { registerPrompts } from './prompts.js';
51
63
  import { REFERENCE_CHEATSHEET } from './reference-cheatsheet.js';
52
64
  import { SCHEMA_VOCABULARY } from './schema-vocab.js';
@@ -107,9 +119,24 @@ interface PreviewPayload {
107
119
  theme?: string;
108
120
  now?: string;
109
121
  width?: number;
122
+ nonWorking?: 'hide' | 'show';
110
123
  locale?: string;
111
124
  }
112
125
 
126
+ /**
127
+ * The preview payload's now-line. An explicit `now` passes through. With only a
128
+ * `timezone`, the payload carries the date that zone resolved to: the preview
129
+ * draws an omitted `now` at the viewer's own date and cannot read the zone.
130
+ */
131
+ function previewNow(
132
+ args: { now?: string; timezone?: string },
133
+ today: Date | undefined,
134
+ ): string | undefined {
135
+ if (args.now !== undefined) return args.now;
136
+ if (args.timezone !== undefined && today) return today.toISOString().slice(0, 10);
137
+ return undefined;
138
+ }
139
+
113
140
  function leanPreviewBlock(payload: PreviewPayload) {
114
141
  return {
115
142
  type: 'text' as const,
@@ -119,6 +146,7 @@ function leanPreviewBlock(payload: PreviewPayload) {
119
146
  theme: payload.theme,
120
147
  now: payload.now,
121
148
  width: payload.width,
149
+ nonWorking: payload.nonWorking,
122
150
  locale: payload.locale,
123
151
  }),
124
152
  };
@@ -185,11 +213,6 @@ function createNodeHostEnv(sourcePath: string): HostEnv {
185
213
 
186
214
  // ---- Helpers ----------------------------------------------------------------
187
215
 
188
- function todayUtc(): Date {
189
- const now = new Date();
190
- return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
191
- }
192
-
193
216
  function exampleShortName(fullName: string): string {
194
217
  return fullName.endsWith('.nowline') ? fullName.slice(0, -'.nowline'.length) : fullName;
195
218
  }
@@ -335,6 +358,43 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
335
358
 
336
359
  registerPrompts(server);
337
360
 
361
+ const nonWorkingSchema = z
362
+ .enum(NON_WORKING_DISPLAYS)
363
+ .optional()
364
+ .describe(
365
+ "How a business calendar's non-working days appear: hide collapses them, " +
366
+ "show draws them as shaded bands. Overrides the file's `default roadmap non-working:` key; " +
367
+ 'omit to let the key apply (then hide). The schedule exports (mermaid, msproj, xlsx) are the same either way.',
368
+ );
369
+
370
+ // A string, not an enum, so `Greyscale` and the `greyscale` alias reach
371
+ // `resolveMcpOptions`, which rejects an unknown name with the theme list.
372
+ const themeSchema = z
373
+ .string()
374
+ .optional()
375
+ .describe(
376
+ `Color theme: ${THEME_NAMES.join(', ')} ("greyscale" is accepted). Defaults to light.`,
377
+ );
378
+
379
+ const nowSchema = z
380
+ .string()
381
+ .regex(/^(\d{4}-\d{2}-\d{2}|-)$/, 'Expected YYYY-MM-DD or "-".')
382
+ .optional()
383
+ .describe(
384
+ "Now-line date as YYYY-MM-DD. Omit to draw it at today's date in `timezone` " +
385
+ "(the server's local zone when `timezone` is omitted too); " +
386
+ 'pass "-" to suppress the now-line (same as the CLI\'s `--now -`).',
387
+ );
388
+
389
+ const timezoneSchema = z
390
+ .string()
391
+ .optional()
392
+ .describe(
393
+ "Timezone that decides today's date when `now` is omitted (ignored when `now` is a date): " +
394
+ '"local" (the default), "UTC", an ISO 8601 offset ("+05:30", "-07:00", "Z"), ' +
395
+ 'or an IANA name ("America/Los_Angeles"). The same forms as the CLI\'s `--timezone`.',
396
+ );
397
+
338
398
  // ---- validate -----------------------------------------------------------
339
399
 
340
400
  server.registerTool(
@@ -349,26 +409,39 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
349
409
  .string()
350
410
  .optional()
351
411
  .describe('Absolute or relative path to a .nowline file to validate.'),
412
+ now: nowSchema,
413
+ timezone: timezoneSchema,
352
414
  }),
353
415
  outputSchema: ValidateOutputSchema,
354
416
  annotations: readOnlyTool('Validate Roadmap'),
355
417
  },
356
418
  async (args) => {
357
419
  try {
420
+ const { options } = resolveMcpOptions(args);
358
421
  const { source, filePath } = await sourceAndPath(args, allowedRoot);
359
422
  const doc = await buildDocument(source);
360
423
  const diagnostics = collectMcpDiagnostics(doc, filePath);
424
+ // One stage: the routed (wave-rule) include diagnostics join
425
+ // the validator's, with the file and line they point into
426
+ // (specs/waves.md §6.1), and the insights come from the same
427
+ // layout, logo included.
428
+ let insights: McpInsight[] = [];
429
+ if (diagnostics.every((d) => d.severity !== 'error')) {
430
+ const stage = await stageMcpRoadmap(
431
+ doc,
432
+ source,
433
+ {
434
+ sourcePath: filePath,
435
+ today: options.today,
436
+ locale: 'en-US',
437
+ theme: 'light',
438
+ },
439
+ createNodeHostEnv(filePath),
440
+ );
441
+ diagnostics.push(...stageRoutedDiagnostics(stage));
442
+ if (stage.kind === 'staged') insights = layoutInsightsFromStage(stage);
443
+ }
361
444
  const ok = diagnostics.every((d) => d.severity !== 'error');
362
- const insights = ok
363
- ? await collectMcpLayoutInsights({
364
- source,
365
- filePath,
366
- today: todayUtc(),
367
- locale: 'en-US',
368
- readFile: createNodeHostEnv(filePath).readSource,
369
- doc,
370
- })
371
- : [];
372
445
  const structured = {
373
446
  ok,
374
447
  diagnostics,
@@ -585,15 +658,16 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
585
658
  .enum(['svg', 'png'])
586
659
  .optional()
587
660
  .describe('Output format. Defaults to svg.'),
588
- theme: z
589
- .enum(['light', 'dark', 'grayscale'])
590
- .optional()
591
- .describe('Color theme. Defaults to light.'),
592
- now: z
593
- .string()
661
+ theme: themeSchema,
662
+ now: nowSchema,
663
+ timezone: timezoneSchema,
664
+ width: z
665
+ .number()
594
666
  .optional()
595
- .describe('Now-line date as YYYY-MM-DD (UTC). Omit to suppress.'),
596
- width: z.number().optional().describe('Canvas width in px.'),
667
+ .describe(
668
+ 'Maximum canvas width in px, an integer of at least 320. Unset fits the content; a wider roadmap shrinks its timeline to fit.',
669
+ ),
670
+ nonWorking: nonWorkingSchema,
597
671
  scale: z
598
672
  .number()
599
673
  .optional()
@@ -631,6 +705,13 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
631
705
  // Only the filesystem touchpoints (reading the input, writing the
632
706
  // output) map to structured NL.MCP.* errors; a kernel render fault
633
707
  // bubbles unchanged so it is never mislabeled as a path/IO failure.
708
+ let resolved: McpResolvedOptions;
709
+ try {
710
+ resolved = resolveMcpOptions(args);
711
+ } catch (err) {
712
+ return handleToolError(err);
713
+ }
714
+ const { theme, options } = resolved;
634
715
  let source: string;
635
716
  let filePath: string;
636
717
  try {
@@ -643,13 +724,14 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
643
724
  if (!blocked.ok) return blocked.response;
644
725
 
645
726
  const format: ExportFormat = args.format ?? 'svg';
646
- const today = args.now ? new Date(`${args.now}T00:00:00Z`) : todayUtc();
727
+ const today = options.today;
647
728
  const inputs: RenderInputs = {
648
729
  sourcePath: filePath,
649
730
  today,
650
731
  locale: 'en-US',
651
- theme: args.theme ?? 'light',
652
- width: args.width,
732
+ theme: options.theme,
733
+ width: options.width,
734
+ nonWorking: options.nonWorking,
653
735
  pngScale: args.scale,
654
736
  };
655
737
  const host = createNodeHostEnv(filePath);
@@ -662,32 +744,49 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
662
744
  const result = await resolveFonts({ headless: true });
663
745
  inputs.fonts = { sans: result.sans, mono: result.mono };
664
746
  }
665
- const bytes = needsRender
666
- ? await exportDocument(source, format, inputs, host)
667
- : new Uint8Array(0);
668
-
669
- const insights = await collectMcpLayoutInsights({
670
- source,
671
- filePath,
672
- today,
673
- theme: args.theme ?? 'light',
674
- width: args.width,
675
- locale: 'en-US',
676
- readFile: host.readSource,
677
- doc: blocked.doc,
678
- });
747
+ // One stage serves the render, the insights and the include check.
748
+ const stage = await stageMcpRoadmap(blocked.doc, source, inputs, host);
749
+ if (stage.kind === 'invalid') {
750
+ return diagnosticsErrorResponse(
751
+ filePath,
752
+ collectMcpDiagnostics(blocked.doc, filePath),
753
+ );
754
+ }
755
+ // Wave-rule include errors are reported as diagnostics. An uncoded
756
+ // include error throws from a render, as the kernel's does; the
757
+ // preview path, which draws nothing here, keeps the preview it
758
+ // returned before waves existed (specs/waves.md §6.1).
759
+ const routed = stageRoutedDiagnostics(stage);
760
+ if (routed.some((d) => d.severity === 'error')) {
761
+ return includeDiagnosticsResponse(blocked.doc, filePath, routed);
762
+ }
763
+ if (stage.kind === 'include-error' && needsRender) acceptStagedRoadmap(stage, inputs);
764
+ const staged = stage.kind === 'staged' ? stage : undefined;
765
+ const insights = staged ? layoutInsightsFromStage(staged) : [];
766
+ const bytes =
767
+ needsRender && staged
768
+ ? await exportStaged(staged, format, inputs, host)
769
+ : new Uint8Array(0);
679
770
 
680
771
  const previewPayload: PreviewPayload = {
681
772
  source,
682
- theme: args.theme,
683
- now: args.now,
684
- width: args.width,
773
+ theme,
774
+ now: previewNow(args, options.today),
775
+ width: options.width,
776
+ nonWorking: options.nonWorking,
685
777
  locale: 'en-US',
686
778
  };
687
779
 
688
780
  const reviewBlocks =
689
781
  args.review === true
690
- ? await buildReviewContentBlocks(source, format, bytes, inputs, host)
782
+ ? await buildReviewContentBlocks(
783
+ blocked.doc,
784
+ source,
785
+ format,
786
+ bytes,
787
+ inputs,
788
+ host,
789
+ )
691
790
  : [];
692
791
 
693
792
  const insightHintBlocks =
@@ -802,10 +901,17 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
802
901
  'Never pass a virtual or sandbox path such as /home/claude/…, /mnt/user-data/…, or any path ' +
803
902
  'outside the configured output folder — those do not exist on the host filesystem.',
804
903
  ),
805
- now: z.string().optional().describe('Now-line date as YYYY-MM-DD (UTC).'),
806
- theme: z.enum(['light', 'dark', 'grayscale']).optional(),
904
+ now: nowSchema,
905
+ timezone: timezoneSchema,
906
+ theme: themeSchema,
907
+ nonWorking: nonWorkingSchema,
807
908
  scale: z.number().optional().describe('PNG scale factor.'),
808
- pageSize: z.string().optional().describe('PDF page size.'),
909
+ pageSize: z
910
+ .string()
911
+ .optional()
912
+ .describe(
913
+ 'PDF page size: a preset name, "content", or <W>x<H><unit> (e.g. 8.5x11in). Validated for every format.',
914
+ ),
809
915
  orientation: z
810
916
  .enum(['portrait', 'landscape', 'auto'])
811
917
  .optional()
@@ -834,6 +940,12 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
834
940
  // Only the filesystem touchpoints (reading the input, writing the
835
941
  // output) map to structured NL.MCP.* errors; a kernel export fault
836
942
  // bubbles unchanged so it is never mislabeled as a path/IO failure.
943
+ let options: SurfaceOptions;
944
+ try {
945
+ ({ options } = resolveMcpOptions(args));
946
+ } catch (err) {
947
+ return handleToolError(err);
948
+ }
837
949
  let source: string;
838
950
  let filePath: string;
839
951
  try {
@@ -846,14 +958,14 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
846
958
  if (!blocked.ok) return blocked.response;
847
959
 
848
960
  const format: ExportFormat = args.format as NonRenderFormat;
849
- const today = args.now ? new Date(`${args.now}T00:00:00Z`) : todayUtc();
850
961
  const inputs: RenderInputs = {
851
962
  sourcePath: filePath,
852
- today,
963
+ today: options.today,
853
964
  locale: 'en-US',
854
- theme: args.theme ?? 'light',
965
+ theme: options.theme,
966
+ nonWorking: options.nonWorking,
855
967
  pngScale: args.scale,
856
- pageSize: args.pageSize,
968
+ pageSize: options.pageSize,
857
969
  orientation: args.orientation,
858
970
  marginPt: args.marginPt,
859
971
  msprojStart: args.msprojStart,
@@ -863,7 +975,21 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
863
975
  inputs.fonts = { sans: result.sans, mono: result.mono };
864
976
  }
865
977
  const host = createNodeHostEnv(filePath);
866
- const bytes = await exportDocument(source, format, inputs, host);
978
+ let bytes: Uint8Array;
979
+ try {
980
+ const staged = acceptStagedRoadmap(
981
+ await stageMcpRoadmap(blocked.doc, source, inputs, host),
982
+ inputs,
983
+ );
984
+ bytes = await exportStaged(staged, format, inputs, host);
985
+ } catch (err) {
986
+ if (!(err instanceof IncludeResolveError)) throw err;
987
+ return includeDiagnosticsResponse(
988
+ blocked.doc,
989
+ filePath,
990
+ routedResolveDiagnosticsToMcp(err.diagnostics),
991
+ );
992
+ }
867
993
 
868
994
  const BINARY_FORMATS = new Set<ExportFormat>(['png', 'pdf', 'xlsx']);
869
995
  const isBinary = BINARY_FORMATS.has(format);
@@ -1051,12 +1177,15 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
1051
1177
  if (args.to === 'json') {
1052
1178
  const { source, filePath } = await sourceAndPath(args, allowedRoot);
1053
1179
  const host = createNodeHostEnv(filePath);
1180
+ // The JSON AST does not depend on today's date, so convert
1181
+ // takes no `now`; the kernel still wants a concrete one.
1182
+ const { options } = resolveMcpOptions({});
1054
1183
  const jsonBytes = await exportDocument(
1055
1184
  source,
1056
1185
  'json',
1057
1186
  {
1058
1187
  sourcePath: filePath,
1059
- today: todayUtc(),
1188
+ today: options.today,
1060
1189
  locale: 'en-US',
1061
1190
  theme: 'light',
1062
1191
  },
@@ -1123,7 +1252,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
1123
1252
  server.registerTool(
1124
1253
  'list-themes',
1125
1254
  {
1126
- description: 'List supported color themes: light, dark, grayscale.',
1255
+ description: `List supported color themes: ${THEME_NAMES.join(', ')}.`,
1127
1256
  inputSchema: z.object({}),
1128
1257
  outputSchema: ListItemsOutputSchema,
1129
1258
  annotations: readOnlyTool('List Themes'),
@@ -1302,7 +1431,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
1302
1431
  'schema',
1303
1432
  {
1304
1433
  description:
1305
- 'Return the structured Nowline DSL key vocabulary (directive keys, entity types, item properties).',
1434
+ 'Return the structured Nowline DSL key vocabulary (directive keys, entity types, item properties, config keywords, calendar declaration keys).',
1306
1435
  inputSchema: z.object({}),
1307
1436
  outputSchema: SchemaOutputSchema,
1308
1437
  annotations: readOnlyTool('View Roadmap Schema'),
@@ -1312,6 +1441,8 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
1312
1441
  directiveKeys: [...SCHEMA_VOCABULARY.directiveKeys],
1313
1442
  entityTypes: [...SCHEMA_VOCABULARY.entityTypes],
1314
1443
  itemPropertyKeys: [...SCHEMA_VOCABULARY.itemPropertyKeys],
1444
+ configKeywords: [...SCHEMA_VOCABULARY.configKeywords],
1445
+ calendarDeclarationKeys: [...SCHEMA_VOCABULARY.calendarDeclarationKeys],
1315
1446
  };
1316
1447
  return {
1317
1448
  content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
@@ -1385,6 +1516,7 @@ function buildExportInlineContentBlocks(
1385
1516
  }
1386
1517
 
1387
1518
  async function buildReviewContentBlocks(
1519
+ doc: Awaited<ReturnType<typeof buildDocument>>,
1388
1520
  source: string,
1389
1521
  format: ExportFormat,
1390
1522
  artifactBytes: Uint8Array,
@@ -1415,7 +1547,13 @@ async function buildReviewContentBlocks(
1415
1547
  const fonts = await resolveFonts({ headless: true });
1416
1548
  reviewInputs.fonts = { sans: fonts.sans, mono: fonts.mono };
1417
1549
  }
1418
- inspectionBytes = await exportDocument(source, 'png', reviewInputs, host);
1550
+ // A narrower review width is a different layout, so it stages again
1551
+ // (the parsed document is reused).
1552
+ const reviewStage = acceptStagedRoadmap(
1553
+ await stageMcpRoadmap(doc, source, reviewInputs, host),
1554
+ reviewInputs,
1555
+ );
1556
+ inspectionBytes = await exportStaged(reviewStage, 'png', reviewInputs, host);
1419
1557
  }
1420
1558
 
1421
1559
  if (format !== 'png' || inspectionBytes.byteLength !== artifactBytes.byteLength) {
package/src/ui/entry.ts CHANGED
@@ -25,12 +25,15 @@
25
25
  declare const __MCP_VERSION__: string;
26
26
 
27
27
  import { App } from '@modelcontextprotocol/ext-apps';
28
+ import { normalizeThemeName } from '@nowline/layout';
28
29
  import { type LivePreviewHandle, mountLivePreview } from '@nowline/preview';
29
30
  import type { NowOverride, ThemeOverride } from '@nowline/preview-shell';
30
31
  import {
31
32
  type PreviewPayload,
32
33
  parsePreviewFromArguments,
33
34
  parsePreviewFromContent,
35
+ toNonWorkingOverride,
36
+ toNowOverride,
34
37
  } from './payload.js';
35
38
 
36
39
  function readPayload(): PreviewPayload | undefined {
@@ -45,16 +48,7 @@ function readPayload(): PreviewPayload | undefined {
45
48
 
46
49
  /** Coerce a raw theme token from the payload to the shell's ThemeOverride. */
47
50
  function toThemeOverride(theme: string | undefined): ThemeOverride {
48
- switch (theme) {
49
- case 'light':
50
- case 'dark':
51
- case 'grayscale':
52
- case 'greyscale':
53
- case 'auto':
54
- return theme;
55
- default:
56
- return 'auto';
57
- }
51
+ return (theme !== undefined ? normalizeThemeName(theme) : undefined) ?? 'auto';
58
52
  }
59
53
 
60
54
  let mounted = false;
@@ -87,7 +81,8 @@ function mountFromPayload(payload: PreviewPayload, app: App): void {
87
81
  mode: hostTheme(app) ?? 'system',
88
82
  initialView: {
89
83
  theme: toThemeOverride(payload.theme),
90
- now: (payload.now ?? 'today') as NowOverride,
84
+ now: toNowOverride(payload.now) as NowOverride,
85
+ nonWorking: toNonWorkingOverride(payload.nonWorking),
91
86
  showLinks: payload.showLinks !== false,
92
87
  },
93
88
  renderOptions: {
package/src/ui/payload.ts CHANGED
@@ -13,6 +13,8 @@ export interface PreviewPayload {
13
13
  theme?: string;
14
14
  now?: string;
15
15
  width?: number;
16
+ /** The render-time non-working display; absent lets the file's own key apply. */
17
+ nonWorking?: 'hide' | 'show';
16
18
  locale?: string;
17
19
  showLinks?: boolean;
18
20
  showMinimap?: boolean;
@@ -53,11 +55,35 @@ export function parsePreviewFromArguments(
53
55
  args: Record<string, unknown> | undefined,
54
56
  ): PreviewPayload | undefined {
55
57
  if (!args || typeof args.source !== 'string') return undefined;
58
+ // A `timezone` without a `now` names a date only the server can resolve;
59
+ // wait for the result payload, which carries it.
60
+ if (typeof args.timezone === 'string' && typeof args.now !== 'string') return undefined;
56
61
  return {
57
62
  source: args.source,
58
63
  theme: typeof args.theme === 'string' ? args.theme : undefined,
59
64
  now: typeof args.now === 'string' ? args.now : undefined,
60
65
  width: typeof args.width === 'number' ? args.width : undefined,
66
+ nonWorking:
67
+ args.nonWorking === 'hide' || args.nonWorking === 'show' ? args.nonWorking : undefined,
61
68
  locale: 'en-US',
62
69
  };
63
70
  }
71
+
72
+ /**
73
+ * The shell's non-working dropdown value for a payload. A payload with no
74
+ * `nonWorking` maps to `'file'` (no override), so the file's own key applies;
75
+ * anything other than `hide` or `show` is treated as unset.
76
+ */
77
+ export function toNonWorkingOverride(nonWorking: unknown): 'file' | 'hide' | 'show' {
78
+ return nonWorking === 'hide' || nonWorking === 'show' ? nonWorking : 'file';
79
+ }
80
+
81
+ /**
82
+ * The shell's now-line value for a payload's `now`. Omitted means today; `-`
83
+ * (the CLI's `--now -` sentinel, also accepted by the `render` tool) hides the
84
+ * now-line; a date passes through.
85
+ */
86
+ export function toNowOverride(now: unknown): string {
87
+ if (now === '-') return 'hide';
88
+ return typeof now === 'string' && now.length > 0 ? now : 'today';
89
+ }