taskchef 7.20.0 → 7.21.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "7.20.0",
3
+ "version": "7.21.0",
4
4
  "description": "Dispatch work from a data-only workspace to visible Codex project tasks.",
5
5
  "author": {
6
6
  "name": "Favo Yang",
package/README.md CHANGED
@@ -55,7 +55,7 @@ The canonical workspace is `~/.agents/taskchef`. TaskChef owns only:
55
55
  ```text
56
56
  AGENTS.md managed dispatcher instructions plus user additions
57
57
  taskchef.json schema-2 Codex project index, dashboard preference, and delegation metadata
58
- tasks.jsonl one task snapshot per line (schema 9; schema 4-8 migration supported)
58
+ tasks.jsonl one task snapshot per line (schema 10; schema 4-9 migration supported)
59
59
  .taskchef-usage.json optional mode-0600 ccusage snapshot and turn-boundary cache
60
60
  ```
61
61
 
@@ -270,7 +270,21 @@ The loopback dashboard watches `tasks.jsonl`, groups current states, and opens
270
270
  linked Codex tasks. List snapshots and SSE events carry only the latest
271
271
  request/result pair; opening task details fetches the full newest-first activity
272
272
  timeline, including clearly labeled interrupted turns.
273
- Task details also offer **Archive chat** for every linked task whose current
273
+ Task details offer infrequent administrative actions without cluttering task
274
+ cards. Selecting **More task actions** (`…`) reveals the action list immediately
275
+ beside the disclosure and changes it to a back/hide control. The list contains
276
+ **Copy Task ID**, direct **Mark completed** and **Mark failed** actions for a
277
+ `working` or `needs_input` task, and **Archive chat** whenever archival is
278
+ eligible. The menu disclosure is the deliberate first step; choosing a terminal
279
+ outcome submits it immediately without a second confirmation. There is no
280
+ free-form reason: the audit turn records a fixed summary, timestamp, dashboard
281
+ provenance, optimistic preconditions, and a unique action ID while preserving
282
+ every executor turn. Terminal tasks cannot be rewritten. Stale or concurrent
283
+ changes are rejected and the dialog refreshes to the current task. A stalled
284
+ local request is aborted after a bounded wait so the dialog cannot remain
285
+ permanently locked; retry keeps the same idempotency identity.
286
+
287
+ The More actions list offers **Archive chat** for every linked task whose current
274
288
  TaskChef state is not `working`. After confirmation, TaskChef invokes only the
275
289
  Codex CLI at the canonical ChatGPT or Codex desktop app location under
276
290
  `/Applications`, to archive the exact thread UUID. The Codex chat leaves active chat lists while the TaskChef record
@@ -303,12 +317,13 @@ revision, so reconnects and non-semantic rewrites do not replay a notice and a
303
317
  later task state cannot rewrite an older notice. A notice remains readable if
304
318
  its task disappears; selecting it then explains that current details are no
305
319
  longer available.
306
- It does not mutate TaskChef data and prints its local URL. A foreground
307
- dashboard identifies itself as standalone and is never reused on the canonical
308
- MCP port. If a standalone, unknown, different-workspace, or stale-version
309
- process owns port 3210, TaskChef reports a concise conflict and never kills or
310
- replaces that process. The foreground CLI similarly asks you to stop the
311
- listener or choose another `--port`.
320
+ Apart from an explicit manual state selection from the task-detail menu, it does not mutate
321
+ TaskChef data and prints its local URL. A foreground dashboard identifies
322
+ itself as standalone and is never reused on the canonical MCP port. If a
323
+ standalone, unknown, different-workspace, or stale-version process owns port
324
+ 3210, TaskChef reports a concise conflict and never kills or replaces that
325
+ process. The foreground CLI similarly asks you to stop the listener or choose
326
+ another `--port`.
312
327
 
313
328
  The health endpoint contains only a fixed service marker, health schema,
314
329
  TaskChef version, dashboard-server version, canonical workspace, and launcher.
@@ -338,9 +353,9 @@ taskchef doctor
338
353
 
339
354
  `doctor` is read-only. `workspace init` creates missing files and refreshes
340
355
  managed instructions. `workspace migrate` explicitly upgrades supported schema
341
- 4-8 task lines to schema 9 under the workspace lock. It validates task and turn
356
+ 4-9 task lines to schema 10 under the workspace lock. It validates task and turn
342
357
  counts plus the complete source and converted log before writing, creates an
343
- exclusive `tasks.jsonl.pre-v9-*.bak`
358
+ exclusive `tasks.jsonl.pre-v10-*.bak`
344
359
  backup, atomically replaces the log, validates the result, and becomes an
345
360
  idempotent no-op after migration. If replacement fails, the original remains
346
361
  or the reported backup can be restored; unsupported or invalid input is rejected
@@ -351,7 +366,7 @@ executor so its first action can retry `link_task`. Do not guess an identity
351
366
  or edit `tasks.jsonl`. If native task creation failed, the record is retained
352
367
  as `failed` with a retained fallback `turnRef` and null thread and Codex turn IDs.
353
368
 
354
- Schemas other than 4, 5, 6, 7, 8, and 9 remain unsupported. Retain such a workspace
369
+ Schemas other than 4, 5, 6, 7, 8, 9, and 10 remain unsupported. Retain such a workspace
355
370
  unchanged and create a current workspace; the migration command deliberately
356
371
  does not guess how to convert unknown formats.
357
372
 
package/docs/spec.md CHANGED
@@ -28,7 +28,7 @@ is dated research, not contract.
28
28
  | **Last semantic result** | The final result-history entry, exposed through the derived `lastResult` compatibility alias. |
29
29
  | **Turn reference** | Required lifecycle identity for one executor prompt. It is the native Codex turn ID when available, otherwise a retained client-generated UUID. |
30
30
  | **Current turn ID** | Optional Codex metadata for the reported prompt; null when native turn reading is unavailable. |
31
- | **Dashboard** | The loopback, read-only UI derived from validated workspace snapshots, optional local usage projections, and bounded native actions. |
31
+ | **Dashboard** | The loopback UI derived from validated workspace snapshots, with optional local usage projections, bounded native actions, and an explicit audited manual-outcome mutation. |
32
32
  | **Skill** | One packaged agent procedure: `taskchef-bootstrap`, `taskchef-dashboard`, `taskchef-delegate`, `taskchef-executor`, or `taskchef-copilot`. |
33
33
 
34
34
  ## Components and ownership
@@ -50,7 +50,8 @@ is dated research, not contract.
50
50
  and the deprecated `report_result` compatibility alias specified below.
51
51
  - The CLI MAY administer and inspect the workspace, but MUST NOT provide a
52
52
  second agent lifecycle protocol.
53
- - The dashboard MUST be read-only with respect to dispatcher files.
53
+ - The dashboard MUST be read-only with respect to dispatcher files except for
54
+ the explicit audited manual-transition operation defined below.
54
55
  - `docs/spec.md` is the single normative behavior source. Other documents
55
56
  MUST link here rather than redefine the contract.
56
57
 
@@ -82,8 +83,8 @@ is backward-compatible and means `true`; new workspaces SHOULD write
82
83
  disable the explicit `ensure_dashboard` tool.
83
84
 
84
85
  `tasks.jsonl` MUST contain zero or more newline-terminated schema-4 through
85
- schema-9 records, one per line. Schemas 4 through 8 are supported
86
- migration/read formats; every new record and state mutation MUST write schema 9. Other schemas
86
+ schema-10 records, one per line. Schemas 4 through 9 are supported
87
+ migration/read formats; every new record and state mutation MUST write schema 10. Other schemas
87
88
  or unsupported fields MUST be rejected without conversion.
88
89
  Reads and writes MUST reject symlinked managed files. Mutations
89
90
  MUST hold the shared workspace lock and replace state atomically; read-only
@@ -95,7 +96,7 @@ Every record MUST contain exactly these fields:
95
96
 
96
97
  | Field | Contract |
97
98
  | --- | --- |
98
- | `schemaVersion` | Integer `9`; schema-4/5/6/7/8 records remain readable until explicit migration or their next mutation. |
99
+ | `schemaVersion` | Integer `10`; schema-4/5/6/7/8/9 records remain readable until explicit migration or their next mutation. |
99
100
  | `id` | Unique safe TaskChef ID; delegation uses a lowercase full UUID. |
100
101
  | `project` | Immutable configured-project snapshot. |
101
102
  | `title` | Non-empty display title. |
@@ -107,14 +108,14 @@ Every record MUST contain exactly these fields:
107
108
  | `turnRef` | Null before reporting begins; otherwise the required lifecycle identity of the current turn. Self-linking journeys use a native or fallback UUID. Migrated low-level `report_result` compatibility records may retain an opaque ref equal to their opaque `turnId`. |
108
109
  | `turnId` | Optional Codex metadata. When non-null it equals `turnRef`; null indicates fallback identity. |
109
110
  | `updatedAt` | ISO 8601 timestamp not earlier than `createdAt` or the prior `updatedAt`; clock rollback cannot backdate a transition. |
110
- | `updatedBy` | `dispatcher` or `mcp`. |
111
- | `turns` | Ordered oldest-first array of `{turnRef, turnId, requestSummary, startedAt, result}`. Every `turnRef` is required. `requestSummary` is null only for migrated/compatibility turns. `result` is null only for the latest working turn, a semantic `{status, summary, updatedAt}` result, or the fixed TaskChef `interrupted` outcome. New self-linking turn refs are unique. A migrated low-level opaque record may retain one final reused native-derived ref for its legacy ambiguity. |
111
+ | `updatedBy` | `dispatcher`, `mcp`, or `dashboard`. `dashboard` is valid only when the latest turn is a manual dashboard transition. |
112
+ | `turns` | Ordered oldest-first array of `{turnRef, turnId, requestSummary, startedAt, result, provenance}`. Every `turnRef` is required. `requestSummary` is null only for migrated/compatibility turns. `result` is null only for the latest working turn, a semantic `{status, summary, updatedAt}` result, or the fixed TaskChef `interrupted` outcome. `provenance` is `{kind: legacy}` or `{kind: mcp}` for ordinary turns, or the audited manual-transition record defined below. New self-linking and manual turn refs are unique. A migrated low-level opaque record may retain one final reused native-derived ref for its legacy ambiguity. |
112
113
 
113
114
  Returned Task objects MUST additionally expose `latestTurn` as null for an empty
114
115
  timeline or the final `turns` entry. They MUST derive `results` only from
115
116
  semantic `needs_input`, `completed`, and `failed` turn results and `lastResult`
116
117
  from the final derived semantic result. Interrupted outcomes MUST be excluded.
117
- These projections MUST NOT be persisted in schema 9 and remain compatibility
118
+ These projections MUST NOT be persisted in schema 10 and remain compatibility
118
119
  aliases for existing callers.
119
120
 
120
121
  ## Optional usage projection
@@ -125,9 +126,15 @@ store, or serve raw Codex rollout files, prompts, responses, transcripts, or
125
126
  reasoning. Analyzer absence, timeout, malformed output, unknown pricing, or an
126
127
  unresolved thread MUST NOT block lifecycle tools or dashboard loading.
127
128
 
129
+ Dashboard-manual turns are administrative events, not Codex executions. They
130
+ MUST immediately report per-turn usage as unavailable, MUST NOT schedule or
131
+ store a cumulative usage boundary, and MUST NOT receive a token or cost delta.
132
+ A later executor turn MUST use the nearest preceding reliable executor
133
+ boundary, skipping any intervening dashboard-manual turns.
134
+
128
135
  The mode-0600 `.taskchef-usage.json` cache stores only normalized cumulative
129
136
  token boundaries, per-turn deltas, model names, estimated cost, source version,
130
- and freshness. It is independent of schema-9 task records so legacy logs remain
137
+ and freshness. It is independent of task-log schema versions so legacy logs remain
131
138
  readable. Writes MUST use the workspace lock and atomic replacement. Symlinked
132
139
  or unsupported cache files MUST be rejected. Writes MUST compact the derived
133
140
  cache to recent task projections, recent per-turn results, and the latest
@@ -350,7 +357,7 @@ new preparation values, though it writes no state.
350
357
 
351
358
  **Structured output:** `{ task: Task }`.
352
359
 
353
- The returned task has schema 9, `working`, null summary/turn/thread/latestTurn/lastResult,
360
+ The returned task has schema 10, `working`, null summary/turn/thread/latestTurn/lastResult,
354
361
  empty `turns` and derived `results` arrays,
355
362
  `updatedBy: dispatcher`, and equal creation/update timestamps. Duplicate IDs,
356
363
  unknown projects, malformed markers, and invalid input fail. Repeating a
@@ -427,8 +434,8 @@ MUST contain no crash output, transcript, user text, or inferred failure cause.
427
434
  temporary compatibility alias. It implicitly accepts a fresh supplied turn and
428
435
  stores its semantic result in a request-unknown turn, including for supported schema-4/5/6 records and
429
436
  low-level opaque direct records. It does not accept `working`. New executor
430
- instructions MUST use `report_state`. Successful mutation upgrades schema 4-8
431
- to schema 9; unsupported schemas remain rejected. Legacy callers that omit
437
+ instructions MUST use `report_state`. Successful mutation upgrades schema 4-9
438
+ to schema 10; unsupported schemas remain rejected. Legacy callers that omit
432
439
  `turnRef` remain compatible when `turnId` is non-null.
433
440
 
434
441
  ## Copilot and dashboard
@@ -476,6 +483,41 @@ startup safely. Direct thread navigation
476
483
  MUST require a canonical Codex UUIDv7. Otherwise it MAY open the revalidated
477
484
  configured project. Project paths from task history MUST be matched against
478
485
  current configuration before use.
486
+ The task-detail dashboard MAY offer manual terminal outcomes only for current
487
+ `working` or `needs_input` tasks. It MUST permit exactly `completed` and
488
+ `failed`, MUST NOT rewrite a terminal task, and MUST keep this infrequent
489
+ administrative action out of list cards. A keyboard-accessible **More task
490
+ actions** disclosure MUST reveal its action list immediately beside it and
491
+ change from an ellipsis to an accessible back/hide control while expanded. The
492
+ list MUST group **Copy Task ID**, **Mark completed**, and **Mark failed** for
493
+ eligible tasks, plus **Archive chat** when archival is eligible. Opening the
494
+ list is the deliberate disclosure step; choosing a terminal outcome submits it
495
+ immediately without a second confirmation. Escape MUST close the idle list,
496
+ pending controls MUST be
497
+ disabled, and failure feedback MUST remain in the dialog. Pending state MUST
498
+ focus and announce a stable status inside the dialog. Failed MUST have
499
+ destructive styling. The client MUST bound and abort a stalled manual-transition
500
+ request, restore the dialog controls, and preserve the action ID so a retry can
501
+ resolve idempotently if the server committed before the timeout.
502
+
503
+ `POST /api/tasks/:id/manual-transition` MUST require the exact loopback origin,
504
+ `application/json`, a bounded body, and exactly this versioned shape:
505
+ `{schemaVersion: 1, actionId, expected: {status, turnRef, threadId, updatedAt},
506
+ targetStatus}`. The server MUST compare every expected field while holding the
507
+ workspace lock. A stale request, invalid transition, or reused action ID with a
508
+ different operation MUST return a conflict without mutation. Replaying the
509
+ same committed action ID and operation MUST return the current task as an
510
+ idempotent success. The operation MUST atomically append one new manual turn;
511
+ it MUST NOT overwrite executor history. An active unfinished turn MUST first
512
+ receive the standard interrupted timeline outcome. The manual turn MUST have a
513
+ new server-generated `turnRef`, null `turnId`, one monotonic timestamp for its
514
+ start, result, and task update, `updatedBy: dashboard`, and deterministic
515
+ request/result summaries. It MUST record provenance
516
+ `{kind: dashboard_manual, actionId, fromStatus, toStatus, expectedTurnRef,
517
+ expectedThreadId, expectedUpdatedAt}`. No free-form reason is collected or
518
+ persisted. A committed write remains successful if the subsequent best-effort
519
+ monitor refresh or notification delivery fails.
520
+
479
521
  The task-detail dashboard MAY offer Codex chat archival only when the stored
480
522
  thread ID is a canonical Codex UUID and the current TaskChef state is not
481
523
  `working`. It MUST revalidate both conditions for the POST request, require the
@@ -521,15 +563,15 @@ its displayed summary when present, event time, and missing-task state.
521
563
 
522
564
  ## Task-log migration
523
565
 
524
- `workspace migrate` MUST explicitly convert every supported schema-4/5/6/7/8 record
566
+ `workspace migrate` MUST explicitly convert every supported schema-4/5/6/7/8/9 record
525
567
  under the shared lock. Each legacy semantic result becomes a request-unknown
526
568
  completed turn; a newer working state becomes a final unfinished turn, and a
527
569
  schema-7/8 timeline is preserved. Each non-null legacy `turnId` becomes the
528
570
  same `turnRef`; each null legacy `turnId` receives one durably persisted UUID.
529
571
  Migration MUST validate the complete source, record/turn counts, turn-ref
530
- invariants, and complete schema-9 candidate before changing the task log,
572
+ invariants, and complete schema-10 candidate before changing the task log,
531
573
  create and read back an exclusive recovery backup, atomically replace the log,
532
- and validate the installed result. A fully schema-9 log MUST be an idempotent
574
+ and validate the installed result. A fully schema-10 log MUST be an idempotent
533
575
  no-op without another backup. Invalid/unsupported input MUST remain untouched;
534
576
  failures after backup creation MUST report the backup path and MUST never
535
577
  partially rewrite individual lines.
package/docs/workflows.md CHANGED
@@ -116,7 +116,7 @@ sequenceDiagram
116
116
  D->>D: Choose one configured and native project
117
117
  D->>M: record_task(id, project, title, instruction, null)
118
118
  M->>W: recordTask()
119
- W->>W: Lock, validate, append schema-9 snapshot
119
+ W->>W: Lock, validate, append schema-10 snapshot
120
120
  W-->>M: working link-pending task
121
121
  M-->>D: task
122
122
  D->>C: Create executor with marked instruction
@@ -260,7 +260,7 @@ sequenceDiagram
260
260
  W->>W: Acquire workspace lock and validate turnRefB is new
261
261
  W->>W: Close unfinished turnA as interrupted
262
262
  W->>W: Append turnB with requestB and null result
263
- W->>F: One atomic schema-9 replacement
263
+ W->>F: One atomic schema-10 replacement
264
264
  W-->>M: working task projected from turnB
265
265
  M-->>E: Idempotent recovery success
266
266
  E->>M: late semantic result for turnA
@@ -396,16 +396,17 @@ all, and ordinary rerendering do not re-announce retained history. Toast action
396
396
  labels remain concise while `aria-describedby` connects the visible summary,
397
397
  event time, and missing-task explanation for assistive technology.
398
398
 
399
- ## Schema 4-8 migration
399
+ ## Schema 4-9 migration
400
400
 
401
401
  `taskchef workspace migrate` acquires the same workspace lock as lifecycle
402
402
  writers, validates the complete legacy log, converts schema-4/5/6 results into
403
403
  request-unknown completed turns and preserves a newer working turn, then validates the
404
404
  complete candidate. Before replacement it writes and reads back an exclusive
405
- `tasks.jsonl.pre-v9-*.bak` file. Schema-7/8 timelines gain a durable `turnRef`:
405
+ `tasks.jsonl.pre-v10-*.bak` file. Schema-7/8 timelines gain a durable `turnRef`:
406
406
  non-null `turnId` values are copied exactly and null IDs receive persisted UUIDs.
407
407
  Task and turn counts plus all refs are validated before and after the atomic
408
- replacement. A second run sees only schema 9 and returns unchanged without
408
+ replacement. Schema-9 turns gain explicit legacy or MCP provenance. A second
409
+ run sees only schema 10 and returns unchanged without
409
410
  another backup. Unsupported or malformed input fails before backup/rewrite;
410
411
  after a later filesystem failure, the reported backup is the recovery source.
411
412
 
@@ -422,9 +423,10 @@ summary is cryptographically authenticated; this is a local single-user trust
422
423
  model. Managed files, instructions, project snapshots, MCP inputs, and dashboard
423
424
  requests are validated at every action boundary.
424
425
 
425
- Configuration schema 2 and task schemas 4 through 9 are accepted. Schemas
426
- 4-8 are read/migration compatibility until an explicit migration or lifecycle
427
- mutation upgrades each record to schema 9. Schema 9 persists `turns`, including
426
+ Configuration schema 2 and task schemas 4 through 10 are accepted. Schemas
427
+ 4-9 are read/migration compatibility until an explicit migration or lifecycle
428
+ mutation upgrades each record to schema 10. Schema 10 persists `turns`, including
428
429
  timeline-only interrupted outcomes, and derives semantic-only `results` and
429
- `lastResult` plus `latestTurn` for compact compatibility. Other schemas
430
+ `lastResult` plus `latestTurn` for compact compatibility. It also records turn
431
+ provenance and the optimistic preconditions for manual dashboard outcomes. Other schemas
430
432
  are rejected without rewrite.
package/index.js CHANGED
@@ -12,6 +12,7 @@ export {
12
12
  initializeWorkspace,
13
13
  linkTask,
14
14
  listProjects,
15
+ manuallyTransitionTask,
15
16
  migrateTaskLog,
16
17
  prepareDispatch,
17
18
  readConfig,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "7.20.0",
3
+ "version": "7.21.0",
4
4
  "description": "A non-blocking interactive dispatcher for visible Codex tasks.",
5
5
  "license": "MIT",
6
6
  "author": "Favo Yang",
@@ -53,3 +53,84 @@ export async function archiveTaskFromControl(event, task, {
53
53
  if (!archived) control.disabled = false;
54
54
  }
55
55
  }
56
+
57
+ export async function manuallyTransitionTaskFromControl(event, task, targetStatus, actionId, {
58
+ clearTimer = globalThis.clearTimeout,
59
+ fetchAction = globalThis.fetch,
60
+ setTimer = globalThis.setTimeout,
61
+ timeoutMs = 10_000,
62
+ } = {}) {
63
+ event?.stopPropagation?.();
64
+ const controller = new AbortController();
65
+ const timeout = setTimer(() => controller.abort(), timeoutMs);
66
+ try {
67
+ const response = await fetchAction(
68
+ `/api/tasks/${encodeURIComponent(task.id)}/manual-transition`,
69
+ {
70
+ method: "POST",
71
+ headers: { "Content-Type": "application/json" },
72
+ body: JSON.stringify({
73
+ schemaVersion: 1,
74
+ actionId,
75
+ expected: {
76
+ status: task.status,
77
+ turnRef: task.turnRef,
78
+ threadId: task.threadId,
79
+ updatedAt: task.updatedAt,
80
+ },
81
+ targetStatus,
82
+ }),
83
+ signal: controller.signal,
84
+ },
85
+ );
86
+ const result = await response.json();
87
+ return response.ok
88
+ ? { ok: true, ...result }
89
+ : {
90
+ ok: false,
91
+ code: result.code ?? "dashboard_error",
92
+ message: result.message ?? "Task state could not be changed.",
93
+ task: result.task ?? null,
94
+ };
95
+ } catch {
96
+ return {
97
+ ok: false,
98
+ code: controller.signal.aborted ? "request_timeout" : "network_error",
99
+ message: controller.signal.aborted
100
+ ? "Task state change timed out. Try again."
101
+ : "Task state changes are temporarily unavailable. Try again.",
102
+ task: null,
103
+ };
104
+ } finally {
105
+ clearTimer(timeout);
106
+ }
107
+ }
108
+
109
+ export function handleManualTransitionEscape(event, {
110
+ active,
111
+ pending,
112
+ cancel,
113
+ }) {
114
+ if (event.key !== "Escape" || !active) return false;
115
+ event.preventDefault();
116
+ event.stopPropagation();
117
+ if (!pending) cancel();
118
+ return true;
119
+ }
120
+
121
+ export function focusManualTransitionStatus(panel) {
122
+ const status = panel.querySelector('[data-manual-focus="pending"]');
123
+ status?.focus();
124
+ return status !== null;
125
+ }
126
+
127
+ export function restoreTaskActionMenuFocus(panel, activeElement, fallback) {
128
+ if (!activeElement || !panel.contains(activeElement)) return null;
129
+ if (!activeElement.hidden && !activeElement.disabled) return activeElement;
130
+ const copyTaskId = panel.querySelector('[data-manual-focus="copy"]');
131
+ const destination = copyTaskId && !copyTaskId.hidden && !copyTaskId.disabled
132
+ ? copyTaskId
133
+ : fallback;
134
+ destination?.focus();
135
+ return destination ?? null;
136
+ }