logwork-cli 0.0.0-stage → 1.0.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 (91) hide show
  1. package/LICENSE +11 -0
  2. package/README.md +287 -2
  3. package/RELEASE.md +89 -0
  4. package/bin/logwork.mjs +101 -0
  5. package/commands/auth.mjs +227 -0
  6. package/commands/delete.mjs +69 -0
  7. package/commands/doctor.mjs +24 -0
  8. package/commands/edit.mjs +88 -0
  9. package/commands/history.mjs +62 -0
  10. package/commands/query.mjs +52 -0
  11. package/commands/quick.mjs +163 -0
  12. package/commands/reconcile.mjs +55 -0
  13. package/commands/release.mjs +77 -0
  14. package/commands/reminder.mjs +172 -0
  15. package/commands/setup.mjs +504 -0
  16. package/commands/tui.mjs +50 -0
  17. package/commands/update.mjs +161 -0
  18. package/docs/advanced.md +334 -0
  19. package/docs/mcp-setup.md +347 -0
  20. package/docs/operations-guide.md +587 -0
  21. package/docs/recipes.md +259 -0
  22. package/docs/security.md +151 -0
  23. package/examples/mcp/antigravity.mcp_config.json +8 -0
  24. package/examples/mcp/codex.config.toml +5 -0
  25. package/examples/mcp/copilot.mcp.json +9 -0
  26. package/lib/api-auth.mjs +1259 -0
  27. package/lib/api.mjs +1422 -0
  28. package/lib/apply-ledger.mjs +227 -0
  29. package/lib/atomic-file.mjs +24 -0
  30. package/lib/auth-diagnostics.mjs +113 -0
  31. package/lib/auth-errors.mjs +22 -0
  32. package/lib/auth-redaction.mjs +78 -0
  33. package/lib/auth-terminal.mjs +97 -0
  34. package/lib/auth.mjs +323 -0
  35. package/lib/batch-parser.mjs +145 -0
  36. package/lib/batch-workflow.mjs +842 -0
  37. package/lib/config.mjs +191 -0
  38. package/lib/credential-store.mjs +130 -0
  39. package/lib/diagnostics.mjs +307 -0
  40. package/lib/entrypoint.mjs +14 -0
  41. package/lib/file-lock.mjs +44 -0
  42. package/lib/http.mjs +125 -0
  43. package/lib/jira-api.mjs +258 -0
  44. package/lib/jira-auth.mjs +203 -0
  45. package/lib/jira-workflow.mjs +523 -0
  46. package/lib/keychain.mjs +105 -0
  47. package/lib/legacy-migration.mjs +66 -0
  48. package/lib/list-navigation.mjs +19 -0
  49. package/lib/logwork-config.mjs +199 -0
  50. package/lib/logwork-f03-enrich.mjs +41 -0
  51. package/lib/macos.mjs +33 -0
  52. package/lib/manual-apply-state.mjs +35 -0
  53. package/lib/manual-commands.mjs +294 -0
  54. package/lib/manual-drafts.mjs +154 -0
  55. package/lib/manual-ink-app.mjs +1654 -0
  56. package/lib/manual-ink-auth.mjs +349 -0
  57. package/lib/manual-ink-ui.mjs +675 -0
  58. package/lib/manual-input.mjs +21 -0
  59. package/lib/manual-logwork-wizard.mjs +310 -0
  60. package/lib/manual-mcp.mjs +187 -0
  61. package/lib/manual-repl.mjs +1037 -0
  62. package/lib/manual-targets.mjs +78 -0
  63. package/lib/mcp-server.mjs +793 -0
  64. package/lib/os-notification.mjs +105 -0
  65. package/lib/package-info.mjs +17 -0
  66. package/lib/paths.mjs +68 -0
  67. package/lib/project-identity.mjs +66 -0
  68. package/lib/project-mapping-workflow.mjs +309 -0
  69. package/lib/project-resolver.mjs +373 -0
  70. package/lib/query-workflow.mjs +553 -0
  71. package/lib/reconciliation-workflow.mjs +446 -0
  72. package/lib/release-doctor.mjs +448 -0
  73. package/lib/reminder-config.mjs +71 -0
  74. package/lib/reminder-scheduler.mjs +301 -0
  75. package/lib/reminder-service.mjs +118 -0
  76. package/lib/reminder-state.mjs +38 -0
  77. package/lib/reminder-workflow.mjs +299 -0
  78. package/lib/ro-delete-args.mjs +38 -0
  79. package/lib/ro-delete-workflow.mjs +115 -0
  80. package/lib/ro-edit-args.mjs +142 -0
  81. package/lib/ro-edit-workflow.mjs +632 -0
  82. package/lib/ro-entry-reader.mjs +56 -0
  83. package/lib/ro-resubmit-workflow.mjs +134 -0
  84. package/lib/terminal-auth-prompts.mjs +120 -0
  85. package/lib/ui.mjs +133 -0
  86. package/lib/update-service.mjs +350 -0
  87. package/lib/util.mjs +66 -0
  88. package/lib/windows-credential-manager.mjs +260 -0
  89. package/lib/worklog-task-resolver.mjs +113 -0
  90. package/package.json +72 -4
  91. package/setup.sh +22 -0
@@ -0,0 +1,1654 @@
1
+ import React, {
2
+ useCallback,
3
+ useMemo,
4
+ useRef,
5
+ useState
6
+ } from 'react';
7
+ import { Box, render, useApp } from 'ink';
8
+ import { isAuthRequiredError } from './auth-errors.mjs';
9
+ import { isJiraAuthRequiredError } from './jira-auth.mjs';
10
+ import {
11
+ buildLogworkText,
12
+ buildCurrentWeekDateOptions,
13
+ buildProjectOverrides,
14
+ buildProjectOptions,
15
+ buildWeekDateOptions,
16
+ canApplyPreview,
17
+ formatDraftPreview,
18
+ parseTaskLine,
19
+ removeDraftTasks,
20
+ replaceDraftTask,
21
+ toggleTaskSelection
22
+ } from './manual-logwork-wizard.mjs';
23
+ import {
24
+ getCommandSuggestions,
25
+ TASK_COMMANDS
26
+ } from './manual-commands.mjs';
27
+ import {
28
+ AUTH_TARGETS,
29
+ BOTH_TARGET,
30
+ jiraLoginInstruction,
31
+ JIRA_TARGET,
32
+ LOGWORK_TARGETS,
33
+ previewTargets,
34
+ RO_TARGET,
35
+ targetLabel
36
+ } from './manual-targets.mjs';
37
+ import {
38
+ deleteManualDraft,
39
+ formatManualDraftLabel,
40
+ loadManualDrafts,
41
+ saveManualDraft
42
+ } from './manual-drafts.mjs';
43
+ import { enrichRoPreviewWithJiraProjects } from './project-mapping-workflow.mjs';
44
+ import {
45
+ CommandInput,
46
+ ConfirmDialog,
47
+ completeCommandValue,
48
+ CurrentPanel,
49
+ DatePicker,
50
+ DraftPicker,
51
+ formatTaskHours,
52
+ HeaderBar,
53
+ LoadingLine,
54
+ loadingMessageForCommand,
55
+ McpClientPicker,
56
+ panelTitleForCommand,
57
+ ProjectPicker,
58
+ sameProjectIdentity,
59
+ StatusBar,
60
+ TargetPicker,
61
+ TaskEditPicker,
62
+ TaskRemovePicker
63
+ } from './manual-ink-ui.mjs';
64
+ import {
65
+ AuthPrompt,
66
+ createInkCredentialProvider
67
+ } from './manual-ink-auth.mjs';
68
+
69
+ export {
70
+ CommandInput,
71
+ ConfirmDialog,
72
+ CurrentPanel,
73
+ DatePicker,
74
+ DraftPicker,
75
+ HeaderBar,
76
+ LoadingLine,
77
+ McpClientPicker,
78
+ OutputPanel,
79
+ ProjectChart,
80
+ ProjectPicker,
81
+ SelectionList,
82
+ SlashMenu,
83
+ StatusBar,
84
+ TargetPicker,
85
+ TaskEditPicker,
86
+ TaskRemovePicker,
87
+ chartLineColor,
88
+ commandItems,
89
+ formatTaskHours,
90
+ loadingMessageForCommand,
91
+ panelColor,
92
+ panelTitleForCommand,
93
+ renderProjectChartText,
94
+ sameProjectIdentity
95
+ } from './manual-ink-ui.mjs';
96
+ export {
97
+ AuthCredentialsPrompt,
98
+ AuthDevicePrompt,
99
+ AuthOtpPrompt,
100
+ AuthPrompt,
101
+ createInkCredentialProvider
102
+ } from './manual-ink-auth.mjs';
103
+
104
+ const h = React.createElement;
105
+ const RECONCILIATION_PERIOD_OPTIONS = [
106
+ { key: 'today', label: 'Today', description: 'Compare today' },
107
+ { key: 'yesterday', label: 'Yesterday', description: 'Compare yesterday' },
108
+ { key: 'this_week', label: 'This week', description: 'Compare Monday through Sunday' },
109
+ { key: 'last_week', label: 'Last week', description: 'Compare the previous week' },
110
+ { key: 'this_month', label: 'This month', description: 'Compare the current month' },
111
+ { key: 'last_month', label: 'Last month', description: 'Compare the previous month' }
112
+ ];
113
+
114
+ function setSessionPreview(session, target, preview) {
115
+ session.activeTarget = target;
116
+ session.previews = {
117
+ ...(session.previews || {}),
118
+ [target]: preview
119
+ };
120
+ }
121
+
122
+ function clearSessionPreview(session, target) {
123
+ if (session.previews) {
124
+ session.previews[target] = null;
125
+ }
126
+ }
127
+
128
+ function clearSessionPreviews(session) {
129
+ session.previews = {
130
+ [RO_TARGET]: null,
131
+ [JIRA_TARGET]: null
132
+ };
133
+ }
134
+
135
+ function sessionPreview(session, target) {
136
+ return session.previews?.[target] || null;
137
+ }
138
+
139
+ function hasSessionPreview(session) {
140
+ return Boolean(session.previews?.[RO_TARGET] || session.previews?.[JIRA_TARGET]);
141
+ }
142
+
143
+ function activeSessionPreview(session) {
144
+ if (session.activeTarget === JIRA_TARGET) {
145
+ return sessionPreview(session, JIRA_TARGET);
146
+ }
147
+ return sessionPreview(session, RO_TARGET);
148
+ }
149
+
150
+ export async function runManualInkApp({
151
+ input,
152
+ output,
153
+ cwd = process.cwd(),
154
+ workflows,
155
+ controller,
156
+ showIntro = true,
157
+ updateNotice = ''
158
+ } = {}) {
159
+ const app = render(h(ManualApp, {
160
+ cwd,
161
+ workflows,
162
+ controller,
163
+ showIntro,
164
+ updateNotice
165
+ }), {
166
+ stdin: input,
167
+ stdout: output,
168
+ stderr: output
169
+ });
170
+ await app.waitUntilExit();
171
+ }
172
+
173
+ export function ManualApp({
174
+ cwd = process.cwd(),
175
+ workflows,
176
+ controller,
177
+ showIntro = true,
178
+ updateNotice = ''
179
+ }) {
180
+ const { exit } = useApp();
181
+ const sessionRef = useRef(controller.createManualSession());
182
+ const confirmResolverRef = useRef(null);
183
+ const authResolverRef = useRef(null);
184
+ const authRejecterRef = useRef(null);
185
+ const [inputValue, setInputValue] = useState('');
186
+ const [selectedIndex, setSelectedIndex] = useState(0);
187
+ const [activePanel, setActivePanel] = useState(() => ({
188
+ kind: 'idle',
189
+ title: 'Ready',
190
+ text: showIntro
191
+ ? ['Type / for commands.', updateNotice].filter(Boolean).join('\n')
192
+ : 'Ready.'
193
+ }));
194
+ const [loading, setLoading] = useState('');
195
+ const [pendingConfirm, setPendingConfirm] = useState(null);
196
+ const [authPrompt, setAuthPrompt] = useState(null);
197
+ const [authActive, setAuthActive] = useState(false);
198
+ const [wizard, setWizard] = useState(null);
199
+
200
+ const prompts = useMemo(() => ({
201
+ confirm(message, initialValue = true) {
202
+ return new Promise((resolveValue) => {
203
+ confirmResolverRef.current = resolveValue;
204
+ setPendingConfirm({
205
+ message,
206
+ initialValue
207
+ });
208
+ });
209
+ }
210
+ }), []);
211
+
212
+ const resolveConfirm = useCallback((value) => {
213
+ const resolver = confirmResolverRef.current;
214
+ confirmResolverRef.current = null;
215
+ setPendingConfirm(null);
216
+ if (resolver) {
217
+ resolver(Boolean(value));
218
+ }
219
+ }, []);
220
+
221
+ const credentialProvider = useMemo(() => createInkCredentialProvider({
222
+ resolverRef: authResolverRef,
223
+ rejecterRef: authRejecterRef,
224
+ setAuthPrompt,
225
+ setActivePanel,
226
+ setInputValue,
227
+ setSelectedIndex
228
+ }), []);
229
+
230
+ const inkWorkflows = useMemo(() => ({
231
+ ...workflows,
232
+ async loginResourceOptimiser(options = {}) {
233
+ setAuthActive(true);
234
+ try {
235
+ return await workflows.loginResourceOptimiser({
236
+ ...options,
237
+ credentialProvider: options.credentialProvider || credentialProvider
238
+ });
239
+ } finally {
240
+ setAuthActive(false);
241
+ setAuthPrompt(null);
242
+ setInputValue('');
243
+ setSelectedIndex(0);
244
+ }
245
+ }
246
+ }), [credentialProvider, workflows]);
247
+
248
+ const resolveAuthPrompt = useCallback((value) => {
249
+ const resolver = authResolverRef.current;
250
+ authResolverRef.current = null;
251
+ authRejecterRef.current = null;
252
+ setAuthPrompt(null);
253
+ setInputValue('');
254
+ setSelectedIndex(0);
255
+ if (resolver) {
256
+ resolver(value);
257
+ }
258
+ }, []);
259
+
260
+ const rejectAuthPrompt = useCallback((error = new Error('Resource Optimiser authentication cancelled.')) => {
261
+ const rejecter = authRejecterRef.current;
262
+ authResolverRef.current = null;
263
+ authRejecterRef.current = null;
264
+ setAuthPrompt(null);
265
+ setInputValue('');
266
+ setSelectedIndex(0);
267
+ if (rejecter) {
268
+ rejecter(error);
269
+ }
270
+ }, []);
271
+
272
+ const runWithInlineAuth = useCallback(async (action) => {
273
+ try {
274
+ return await action();
275
+ } catch (error) {
276
+ if (!isAuthRequiredError(error)) {
277
+ throw error;
278
+ }
279
+ setActivePanel({
280
+ kind: 'auth',
281
+ title: 'Authentication',
282
+ text: 'Resource Optimiser authentication required. Enter credentials in this CLI panel.'
283
+ });
284
+ setLoading('Authenticating Resource Optimiser...');
285
+ await inkWorkflows.loginResourceOptimiser();
286
+ return action();
287
+ }
288
+ }, [inkWorkflows]);
289
+
290
+ const runWithJiraAuthInstruction = useCallback(async (action) => {
291
+ try {
292
+ return await action();
293
+ } catch (error) {
294
+ if (!isJiraAuthRequiredError(error)) {
295
+ throw error;
296
+ }
297
+ throw new Error(jiraLoginInstruction());
298
+ }
299
+ }, []);
300
+
301
+ const setCommandPanel = useCallback((title, kind = 'output') => {
302
+ const lines = [];
303
+ setActivePanel({
304
+ kind,
305
+ title,
306
+ text: ''
307
+ });
308
+ return (message = '') => {
309
+ lines.push(String(message ?? ''));
310
+ setActivePanel({
311
+ kind,
312
+ title,
313
+ text: lines.join('\n')
314
+ });
315
+ };
316
+ }, []);
317
+
318
+ const executeCommand = useCallback(async (command) => {
319
+ if (command.type !== 'apply' && command.type !== 'diagnostics') {
320
+ setWizard(null);
321
+ }
322
+ setLoading(loadingMessageForCommand(command));
323
+ const print = setCommandPanel(panelTitleForCommand(command), command.type === 'apply' ? 'success' : 'output');
324
+ try {
325
+ const result = await controller.executeManualCommand(command, sessionRef.current, {
326
+ cwd,
327
+ workflows: inkWorkflows,
328
+ prompts,
329
+ print,
330
+ readLine: null
331
+ });
332
+ if (result.exit) {
333
+ exit();
334
+ }
335
+ } catch (error) {
336
+ setActivePanel({
337
+ kind: 'error',
338
+ title: 'Error',
339
+ text: error.message
340
+ });
341
+ } finally {
342
+ if (command.type === 'apply' && !hasSessionPreview(sessionRef.current)) {
343
+ sessionRef.current.logworkDraft = null;
344
+ }
345
+ setLoading('');
346
+ }
347
+ }, [controller, cwd, exit, inkWorkflows, prompts, setCommandPanel]);
348
+
349
+ const startNewLogworkWizard = useCallback(async (target = RO_TARGET) => {
350
+ setInputValue('');
351
+ setSelectedIndex(0);
352
+ setLoading(target === JIRA_TARGET ? 'Building current week...' : 'Fetching current week...');
353
+ setActivePanel({
354
+ kind: 'logwork',
355
+ title: 'Logwork',
356
+ text: target === JIRA_TARGET
357
+ ? 'Preparing Jira worklog dates...'
358
+ : 'Loading this week booking/logwork data...'
359
+ });
360
+ try {
361
+ const weekly = target === JIRA_TARGET
362
+ ? null
363
+ : await runWithInlineAuth(() => inkWorkflows.queryLogwork({
364
+ period: 'this_week',
365
+ cwd,
366
+ includeEntries: false
367
+ }));
368
+ const dateOptions = weekly ? buildWeekDateOptions(weekly) : buildCurrentWeekDateOptions();
369
+ if (!dateOptions.length) {
370
+ throw new Error('Unable to build week date options.');
371
+ }
372
+ const todayIndex = dateOptions.findIndex((option) => option.isToday);
373
+ setWizard({
374
+ step: 'select_date',
375
+ target,
376
+ weekly,
377
+ dateOptions,
378
+ dateIndex: todayIndex >= 0 ? todayIndex : 0,
379
+ projectsResult: null,
380
+ projectOptions: [],
381
+ projectIndex: 0,
382
+ selectedDate: null,
383
+ selectedProject: null,
384
+ tasks: [],
385
+ preview: null,
386
+ activeDraftId: null
387
+ });
388
+ setActivePanel({
389
+ kind: 'logwork',
390
+ title: 'Select Date',
391
+ text: `Choose a day for this ${targetLabel(target)} logwork session.`
392
+ });
393
+ } catch (error) {
394
+ setActivePanel({
395
+ kind: 'error',
396
+ title: 'Logwork Error',
397
+ text: error.message
398
+ });
399
+ } finally {
400
+ setLoading('');
401
+ }
402
+ }, [cwd, inkWorkflows, runWithInlineAuth, runWithJiraAuthInstruction]);
403
+
404
+ const startLogworkTargetPicker = useCallback(() => {
405
+ setInputValue('');
406
+ setSelectedIndex(0);
407
+ setWizard({
408
+ step: 'select_logwork_target',
409
+ targetIndex: 0
410
+ });
411
+ setActivePanel({
412
+ kind: 'logwork',
413
+ title: 'Select Logwork Target',
414
+ text: 'Choose where this logwork session should write.'
415
+ });
416
+ }, []);
417
+
418
+ const startAuthTargetPicker = useCallback(() => {
419
+ setInputValue('');
420
+ setSelectedIndex(0);
421
+ setWizard({
422
+ step: 'select_auth_target',
423
+ targetIndex: 0
424
+ });
425
+ setActivePanel({
426
+ kind: 'auth',
427
+ title: 'Select Auth Target',
428
+ text: 'Choose which service to authenticate.'
429
+ });
430
+ }, []);
431
+
432
+ const startReconciliationPeriodPicker = useCallback(() => {
433
+ setInputValue('');
434
+ setSelectedIndex(0);
435
+ setWizard({
436
+ step: 'select_reconciliation_period',
437
+ periodIndex: 2
438
+ });
439
+ setActivePanel({
440
+ kind: 'output',
441
+ title: 'RO / Jira Reconciliation',
442
+ text: 'Choose a preset period to compare.'
443
+ });
444
+ }, []);
445
+
446
+ const startLogworkWizard = useCallback(async (target) => {
447
+ if (!target) {
448
+ startLogworkTargetPicker();
449
+ return;
450
+ }
451
+
452
+ if (target !== RO_TARGET) {
453
+ await startNewLogworkWizard(target);
454
+ return;
455
+ }
456
+
457
+ setInputValue('');
458
+ setSelectedIndex(0);
459
+ setLoading('Loading saved drafts...');
460
+ try {
461
+ const drafts = await loadManualDrafts({ cwd });
462
+ if (drafts.length) {
463
+ setWizard({
464
+ step: 'select_draft',
465
+ target: RO_TARGET,
466
+ drafts,
467
+ draftIndex: 0,
468
+ draftSource: 'start',
469
+ weekly: null,
470
+ dateOptions: [],
471
+ dateIndex: 0,
472
+ projectsResult: null,
473
+ projectOptions: [],
474
+ projectIndex: 0,
475
+ selectedDate: null,
476
+ selectedProject: null,
477
+ tasks: [],
478
+ preview: null,
479
+ activeDraftId: null
480
+ });
481
+ setActivePanel({
482
+ kind: 'logwork',
483
+ title: 'Saved Drafts',
484
+ text: 'Resume a saved draft, delete one, or start a new logwork session.'
485
+ });
486
+ return;
487
+ }
488
+ await startNewLogworkWizard(RO_TARGET);
489
+ } catch (error) {
490
+ setActivePanel({
491
+ kind: 'error',
492
+ title: 'Draft Error',
493
+ text: error.message
494
+ });
495
+ } finally {
496
+ setLoading('');
497
+ }
498
+ }, [cwd, startLogworkTargetPicker, startNewLogworkWizard]);
499
+
500
+ const startMcpClientPicker = useCallback(() => {
501
+ setInputValue('');
502
+ setSelectedIndex(0);
503
+ setWizard({
504
+ step: 'select_mcp_client',
505
+ mcpClientIndex: 0
506
+ });
507
+ setActivePanel({
508
+ kind: 'output',
509
+ title: 'MCP Setup',
510
+ text: 'Choose an MCP client to print copy-ready setup.'
511
+ });
512
+ }, []);
513
+
514
+ const cancelMcpClientPicker = useCallback(() => {
515
+ setWizard(null);
516
+ setActivePanel({
517
+ kind: 'idle',
518
+ title: 'Ready',
519
+ text: 'Type / for commands.'
520
+ });
521
+ }, []);
522
+
523
+ const selectWizardDate = useCallback(async (dateOption) => {
524
+ const target = wizard?.target || RO_TARGET;
525
+ if (target === JIRA_TARGET) {
526
+ const draft = {
527
+ target,
528
+ date: dateOption.date,
529
+ project: null,
530
+ tasks: [],
531
+ preview: null
532
+ };
533
+ sessionRef.current.logworkDraft = draft;
534
+ clearSessionPreviews(sessionRef.current);
535
+ sessionRef.current.activeTarget = target;
536
+ setWizard((current) => ({
537
+ ...current,
538
+ step: 'edit_tasks',
539
+ selectedDate: dateOption.date,
540
+ selectedProject: null,
541
+ tasks: [],
542
+ preview: null,
543
+ previews: {}
544
+ }));
545
+ setActivePanel({
546
+ kind: 'logwork',
547
+ title: 'Jira Worklog Draft',
548
+ text: [
549
+ formatDraftPreview(draft),
550
+ '',
551
+ 'Enter tasks one per line: +2 Feature Development | check ui/ux (SCB-213)',
552
+ 'Empty Enter applies after preview is ready.',
553
+ 'Type / for task commands. Esc cancels.'
554
+ ].join('\n')
555
+ });
556
+ return;
557
+ }
558
+
559
+ setLoading('Fetching project memberships...');
560
+ setActivePanel({
561
+ kind: 'logwork',
562
+ title: 'Select Project',
563
+ text: `Loading projects for ${dateOption.date}...`
564
+ });
565
+ try {
566
+ const projectsResult = await runWithInlineAuth(() => inkWorkflows.listLogworkProjects({ cwd }));
567
+ const projectOptions = buildProjectOptions({
568
+ projectsResult,
569
+ weekly: wizard.weekly,
570
+ date: dateOption.date
571
+ });
572
+ if (!projectOptions.length) {
573
+ throw new Error('No Resource Optimiser project memberships found.');
574
+ }
575
+ setWizard((current) => ({
576
+ ...current,
577
+ step: 'select_project',
578
+ target,
579
+ selectedDate: dateOption.date,
580
+ projectsResult,
581
+ projectOptions,
582
+ projectIndex: 0,
583
+ selectedProject: null,
584
+ tasks: [],
585
+ preview: null
586
+ }));
587
+ setActivePanel({
588
+ kind: 'logwork',
589
+ title: 'Select Project',
590
+ text: `Choose a project for ${dateOption.date}.`
591
+ });
592
+ } catch (error) {
593
+ setActivePanel({
594
+ kind: 'error',
595
+ title: 'Project Error',
596
+ text: error.message
597
+ });
598
+ } finally {
599
+ setLoading('');
600
+ }
601
+ }, [cwd, inkWorkflows, runWithInlineAuth, wizard]);
602
+
603
+ const selectWizardProject = useCallback((project) => {
604
+ const target = wizard?.target || RO_TARGET;
605
+ const draft = {
606
+ target,
607
+ date: wizard.selectedDate,
608
+ project,
609
+ tasks: [],
610
+ preview: null
611
+ };
612
+ sessionRef.current.logworkDraft = draft;
613
+ clearSessionPreviews(sessionRef.current);
614
+ sessionRef.current.activeTarget = target;
615
+ setWizard((current) => ({
616
+ ...current,
617
+ step: 'edit_tasks',
618
+ target,
619
+ selectedProject: project,
620
+ tasks: [],
621
+ preview: null,
622
+ previews: {}
623
+ }));
624
+ setActivePanel({
625
+ kind: 'logwork',
626
+ title: target === BOTH_TARGET ? 'RO + Jira Draft' : 'Logwork Draft',
627
+ text: [
628
+ formatDraftPreview(draft),
629
+ '',
630
+ target === BOTH_TARGET ? 'Enter tasks one per line: +2 Feature Development | check ui/ux (SCB-213)' : 'Enter tasks one per line: +2 Feature Development | check ui/ux',
631
+ 'Empty Enter applies after preview is ready.',
632
+ 'Type / for task commands. Esc cancels.'
633
+ ].join('\n')
634
+ });
635
+ }, [wizard]);
636
+
637
+ const previewDraft = useCallback(async (nextWizard) => {
638
+ const target = nextWizard.target || RO_TARGET;
639
+ const draft = {
640
+ target,
641
+ date: nextWizard.selectedDate,
642
+ project: nextWizard.selectedProject,
643
+ tasks: nextWizard.tasks
644
+ };
645
+
646
+ if (!draft.tasks.length) {
647
+ sessionRef.current.logworkDraft = draft;
648
+ clearSessionPreviews(sessionRef.current);
649
+ sessionRef.current.activeTarget = target;
650
+ setWizard(nextWizard);
651
+ setActivePanel({
652
+ kind: 'logwork',
653
+ title: target === JIRA_TARGET ? 'Jira Worklog Draft' : target === BOTH_TARGET ? 'RO + Jira Draft' : 'Logwork Draft',
654
+ text: [
655
+ formatDraftPreview(draft),
656
+ '',
657
+ target === RO_TARGET ? 'Enter tasks one per line: +2 Feature Development | check ui/ux' : 'Enter tasks one per line: +2 Feature Development | check ui/ux (SCB-213)',
658
+ 'Empty Enter applies after preview is ready.'
659
+ ].join('\n')
660
+ });
661
+ return;
662
+ }
663
+
664
+ setLoading('Building live preview...');
665
+ try {
666
+ const text = buildLogworkText({
667
+ date: draft.date,
668
+ tasks: draft.tasks
669
+ });
670
+ const previews = {};
671
+ if (previewTargets(target).includes(RO_TARGET)) {
672
+ const projectOverrides = buildProjectOverrides({
673
+ date: draft.date,
674
+ tasks: draft.tasks,
675
+ projectMemberId: draft.project.projectMemberId
676
+ });
677
+ previews[RO_TARGET] = await runWithInlineAuth(() => inkWorkflows.previewLogworkBatch({
678
+ text,
679
+ projectOverrides,
680
+ cwd
681
+ }));
682
+ }
683
+ if (previewTargets(target).includes(JIRA_TARGET)) {
684
+ previews[JIRA_TARGET] = await runWithJiraAuthInstruction(() => inkWorkflows.previewJiraWorklogBatch({
685
+ text
686
+ }));
687
+ }
688
+ if (target === BOTH_TARGET && previews[RO_TARGET] && previews[JIRA_TARGET]) {
689
+ previews[RO_TARGET] = enrichRoPreviewWithJiraProjects(previews[RO_TARGET], previews[JIRA_TARGET]);
690
+ }
691
+ const preview = target === JIRA_TARGET ? previews[JIRA_TARGET] : previews[RO_TARGET];
692
+ const withPreview = {
693
+ ...draft,
694
+ preview,
695
+ previews
696
+ };
697
+ sessionRef.current.logworkDraft = withPreview;
698
+ sessionRef.current.activeTarget = target;
699
+ for (const previewTarget of previewTargets(target)) {
700
+ setSessionPreview(sessionRef.current, previewTarget, previews[previewTarget] || null);
701
+ }
702
+ setWizard({
703
+ ...nextWizard,
704
+ target,
705
+ preview,
706
+ previews
707
+ });
708
+ const hasBlockedJira = previews[JIRA_TARGET]?.entries?.some((entry) => entry.status !== 'ready');
709
+ setActivePanel({
710
+ kind: preview?.unresolvedEntries?.length || preview?.errors?.length || hasBlockedJira ? 'error' : 'logwork',
711
+ title: target === JIRA_TARGET ? 'Jira Worklog Preview' : target === BOTH_TARGET ? 'RO + Jira Preview' : 'Logwork Preview',
712
+ text: [
713
+ formatDraftPreview(withPreview),
714
+ '',
715
+ 'Empty Enter applies this preview.',
716
+ 'Type another +hours task to add more.',
717
+ '/remove selects tasks to delete.'
718
+ ].join('\n')
719
+ });
720
+ return preview;
721
+ } catch (error) {
722
+ setActivePanel({
723
+ kind: 'error',
724
+ title: 'Preview Error',
725
+ text: error.message
726
+ });
727
+ } finally {
728
+ setLoading('');
729
+ }
730
+ }, [cwd, inkWorkflows, runWithInlineAuth, runWithJiraAuthInstruction]);
731
+
732
+ const saveCurrentDraft = useCallback(async () => {
733
+ if (wizard?.target === JIRA_TARGET) {
734
+ setActivePanel({
735
+ kind: 'logwork',
736
+ title: 'Save Draft',
737
+ text: 'Saved drafts are only supported for Resource Optimiser logwork in this version.'
738
+ });
739
+ return null;
740
+ }
741
+
742
+ if (!wizard?.selectedDate || !wizard?.selectedProject || !wizard?.tasks?.length) {
743
+ setActivePanel({
744
+ kind: 'logwork',
745
+ title: 'Save Draft',
746
+ text: 'Add at least one task before saving a draft.'
747
+ });
748
+ return null;
749
+ }
750
+
751
+ try {
752
+ const saved = await saveManualDraft({
753
+ id: wizard.activeDraftId,
754
+ cwd,
755
+ date: wizard.selectedDate,
756
+ project: wizard.selectedProject,
757
+ tasks: wizard.tasks,
758
+ latestPreviewStatus: wizard.preview?.status
759
+ }, { cwd });
760
+ setWizard((current) => ({
761
+ ...current,
762
+ activeDraftId: saved.id
763
+ }));
764
+ setActivePanel({
765
+ kind: 'success',
766
+ title: 'Draft Saved',
767
+ text: [
768
+ `Saved ${saved.tasks.length} task${saved.tasks.length === 1 ? '' : 's'} locally.`,
769
+ formatManualDraftLabel(saved),
770
+ '',
771
+ 'Continue editing, press empty Enter to apply, or type /drafts.'
772
+ ].join('\n')
773
+ });
774
+ return saved;
775
+ } catch (error) {
776
+ setActivePanel({
777
+ kind: 'error',
778
+ title: 'Save Draft Error',
779
+ text: error.message
780
+ });
781
+ return null;
782
+ }
783
+ }, [cwd, wizard]);
784
+
785
+ const openDraftPicker = useCallback(async (source = 'editor') => {
786
+ setLoading('Loading saved drafts...');
787
+ try {
788
+ const drafts = await loadManualDrafts({ cwd });
789
+ if (!drafts.length) {
790
+ setActivePanel({
791
+ kind: 'logwork',
792
+ title: 'Saved Drafts',
793
+ text: 'No saved drafts found.'
794
+ });
795
+ return;
796
+ }
797
+ setWizard((current) => ({
798
+ ...current,
799
+ step: 'select_draft',
800
+ drafts,
801
+ draftIndex: 0,
802
+ draftSource: source
803
+ }));
804
+ setActivePanel({
805
+ kind: 'logwork',
806
+ title: 'Saved Drafts',
807
+ text: 'Enter resumes · d deletes · Esc returns.'
808
+ });
809
+ } catch (error) {
810
+ setActivePanel({
811
+ kind: 'error',
812
+ title: 'Draft Error',
813
+ text: error.message
814
+ });
815
+ } finally {
816
+ setLoading('');
817
+ }
818
+ }, [cwd]);
819
+
820
+ const resumeManualDraft = useCallback(async (draft) => {
821
+ setLoading('Resuming saved draft...');
822
+ try {
823
+ const weekly = await runWithInlineAuth(() => inkWorkflows.queryLogwork({
824
+ period: 'this_week',
825
+ cwd,
826
+ includeEntries: false
827
+ }));
828
+ const dateOptions = buildWeekDateOptions(weekly);
829
+ const projectsResult = await runWithInlineAuth(() => inkWorkflows.listLogworkProjects({ cwd }));
830
+ const projectOptions = buildProjectOptions({
831
+ projectsResult,
832
+ weekly,
833
+ date: draft.date
834
+ });
835
+ const selectedProject = projectOptions.find((project) => sameProjectIdentity(project, draft.project)) || draft.project;
836
+ const nextWizard = {
837
+ step: 'edit_tasks',
838
+ target: RO_TARGET,
839
+ weekly,
840
+ dateOptions,
841
+ dateIndex: Math.max(0, dateOptions.findIndex((option) => option.date === draft.date)),
842
+ projectsResult,
843
+ projectOptions,
844
+ projectIndex: Math.max(0, projectOptions.findIndex((project) => sameProjectIdentity(project, selectedProject))),
845
+ selectedDate: draft.date,
846
+ selectedProject,
847
+ tasks: draft.tasks,
848
+ preview: null,
849
+ activeDraftId: draft.id
850
+ };
851
+ sessionRef.current.logworkDraft = {
852
+ target: RO_TARGET,
853
+ date: draft.date,
854
+ project: selectedProject,
855
+ tasks: draft.tasks,
856
+ preview: null
857
+ };
858
+ clearSessionPreviews(sessionRef.current);
859
+ sessionRef.current.activeTarget = RO_TARGET;
860
+ await previewDraft(nextWizard);
861
+ } catch (error) {
862
+ setActivePanel({
863
+ kind: 'error',
864
+ title: 'Resume Draft Error',
865
+ text: error.message
866
+ });
867
+ } finally {
868
+ setLoading('');
869
+ }
870
+ }, [cwd, inkWorkflows, previewDraft, runWithInlineAuth]);
871
+
872
+ const deleteSavedDraft = useCallback(async (draft) => {
873
+ const ok = await prompts.confirm(`Delete saved draft "${formatManualDraftLabel(draft)}"?`, false);
874
+ if (!ok) {
875
+ return;
876
+ }
877
+ await deleteManualDraft(draft.id);
878
+ const drafts = await loadManualDrafts({ cwd });
879
+ setWizard((current) => ({
880
+ ...current,
881
+ drafts,
882
+ draftIndex: 0
883
+ }));
884
+ setActivePanel({
885
+ kind: 'success',
886
+ title: 'Draft Deleted',
887
+ text: drafts.length ? 'Draft deleted. Choose another saved draft or start new.' : 'Draft deleted. No saved drafts remain.'
888
+ });
889
+ }, [cwd, prompts]);
890
+
891
+ const returnFromDraftPicker = useCallback(() => {
892
+ if (wizard?.draftSource === 'start') {
893
+ startNewLogworkWizard(RO_TARGET);
894
+ return;
895
+ }
896
+ setWizard((current) => ({
897
+ ...current,
898
+ step: 'edit_tasks',
899
+ draftSource: undefined
900
+ }));
901
+ const draft = {
902
+ target: wizard?.target || RO_TARGET,
903
+ date: wizard?.selectedDate,
904
+ project: wizard?.selectedProject,
905
+ tasks: wizard?.tasks || [],
906
+ preview: wizard?.preview
907
+ };
908
+ setActivePanel({
909
+ kind: 'logwork',
910
+ title: wizard?.preview ? 'Logwork Preview' : 'Logwork Draft',
911
+ text: [
912
+ formatDraftPreview(draft),
913
+ '',
914
+ 'Type / for task commands. Empty Enter applies when ready.'
915
+ ].join('\n')
916
+ });
917
+ }, [startNewLogworkWizard, wizard]);
918
+
919
+ const cancelCurrentLogwork = useCallback(async () => {
920
+ if (wizard?.tasks?.length) {
921
+ const ok = await prompts.confirm('Discard this unsaved logwork session?', false);
922
+ if (!ok) {
923
+ return;
924
+ }
925
+ }
926
+ sessionRef.current.logworkDraft = null;
927
+ clearSessionPreviews(sessionRef.current);
928
+ setWizard(null);
929
+ setActivePanel({
930
+ kind: 'idle',
931
+ title: 'Ready',
932
+ text: 'Logwork session discarded. Type / for commands.'
933
+ });
934
+ }, [prompts, wizard]);
935
+
936
+ const applyWizardPreview = useCallback(async () => {
937
+ const target = wizard.target || RO_TARGET;
938
+ if (!wizard.tasks.length) {
939
+ setActivePanel({
940
+ kind: 'logwork',
941
+ title: 'Logwork Draft',
942
+ text: target === RO_TARGET ? 'Enter a task like +2 Feature Development | check ui/ux.' : 'Enter a task like +2 Feature Development | check ui/ux (SCB-213).'
943
+ });
944
+ return;
945
+ }
946
+
947
+ const targets = previewTargets(target);
948
+ const missingPreview = targets.some((previewTarget) => !sessionPreview(sessionRef.current, previewTarget));
949
+ if (missingPreview) {
950
+ await previewDraft(wizard);
951
+ return;
952
+ }
953
+
954
+ for (const previewTarget of targets) {
955
+ const applyState = canApplyPreview(sessionPreview(sessionRef.current, previewTarget), { target: previewTarget });
956
+ if (!applyState.ok) {
957
+ setActivePanel({
958
+ kind: 'error',
959
+ title: 'Apply Blocked',
960
+ text: `${targetLabel(previewTarget)}: ${applyState.reason}`
961
+ });
962
+ return;
963
+ }
964
+ }
965
+
966
+ await executeCommand({ type: 'apply', target });
967
+ if (!hasSessionPreview(sessionRef.current)) {
968
+ setWizard(null);
969
+ }
970
+ }, [executeCommand, previewDraft, wizard]);
971
+
972
+ const restoreTaskEditorPanel = useCallback((nextWizard = wizard) => {
973
+ const target = nextWizard.target || RO_TARGET;
974
+ const draft = {
975
+ target,
976
+ date: nextWizard.selectedDate,
977
+ project: nextWizard.selectedProject,
978
+ tasks: nextWizard.tasks,
979
+ preview: nextWizard.preview,
980
+ previews: nextWizard.previews
981
+ };
982
+ setActivePanel({
983
+ kind: 'logwork',
984
+ title: nextWizard.preview ? `${targetLabel(target)} Preview` : `${targetLabel(target)} Draft`,
985
+ text: [
986
+ formatDraftPreview(draft),
987
+ '',
988
+ nextWizard.preview ? 'Empty Enter applies this preview.' : target === RO_TARGET ? 'Enter tasks one per line: +2 Feature Development | check ui/ux' : 'Enter tasks one per line: +2 Feature Development | check ui/ux (SCB-213)',
989
+ nextWizard.preview ? 'Type another +hours task to add more.' : 'Empty Enter applies after preview is ready.',
990
+ 'Type / for task commands. Esc cancels.'
991
+ ].join('\n')
992
+ });
993
+ }, [wizard]);
994
+
995
+ const cancelRemoveTasks = useCallback(() => {
996
+ setWizard((current) => ({
997
+ ...current,
998
+ step: 'edit_tasks',
999
+ removeIndex: 0,
1000
+ removeSelectedIndexes: []
1001
+ }));
1002
+ restoreTaskEditorPanel();
1003
+ }, [restoreTaskEditorPanel]);
1004
+
1005
+ const confirmRemoveTasks = useCallback(async (selectedIndexes) => {
1006
+ if (!selectedIndexes.length) {
1007
+ setActivePanel({
1008
+ kind: 'logwork',
1009
+ title: 'Remove Tasks',
1010
+ text: 'No tasks selected. Use Space to select tasks, Enter to remove, Esc to cancel.'
1011
+ });
1012
+ return;
1013
+ }
1014
+
1015
+ const ok = await prompts.confirm(`Remove ${selectedIndexes.length} selected task${selectedIndexes.length === 1 ? '' : 's'}?`);
1016
+ if (!ok) {
1017
+ cancelRemoveTasks();
1018
+ return;
1019
+ }
1020
+
1021
+ await previewDraft({
1022
+ ...wizard,
1023
+ step: 'edit_tasks',
1024
+ tasks: removeDraftTasks(wizard.tasks, selectedIndexes),
1025
+ preview: null,
1026
+ removeIndex: 0,
1027
+ removeSelectedIndexes: []
1028
+ });
1029
+ }, [cancelRemoveTasks, previewDraft, prompts, wizard]);
1030
+
1031
+ const submitWizardTaskInput = useCallback(async (value) => {
1032
+ const text = String(value || '').trim();
1033
+ setInputValue('');
1034
+
1035
+ if (!text) {
1036
+ await applyWizardPreview();
1037
+ return;
1038
+ }
1039
+
1040
+ if (text === '/diagnostics') {
1041
+ await executeCommand({ type: 'diagnostics' });
1042
+ return;
1043
+ }
1044
+
1045
+ if (text === '/cancel' || text === '/discard') {
1046
+ await cancelCurrentLogwork();
1047
+ return;
1048
+ }
1049
+
1050
+ if (text === '/back') {
1051
+ sessionRef.current.logworkDraft = null;
1052
+ clearSessionPreviews(sessionRef.current);
1053
+ if (wizard.target === JIRA_TARGET) {
1054
+ setWizard((current) => ({
1055
+ ...current,
1056
+ step: 'select_date',
1057
+ selectedDate: null,
1058
+ tasks: [],
1059
+ preview: null,
1060
+ previews: {}
1061
+ }));
1062
+ setActivePanel({
1063
+ kind: 'logwork',
1064
+ title: 'Select Date',
1065
+ text: 'Choose a day for this Jira worklog session.'
1066
+ });
1067
+ } else {
1068
+ setWizard((current) => ({
1069
+ ...current,
1070
+ step: 'select_project',
1071
+ selectedProject: null,
1072
+ tasks: [],
1073
+ preview: null,
1074
+ previews: {}
1075
+ }));
1076
+ setActivePanel({
1077
+ kind: 'logwork',
1078
+ title: 'Select Project',
1079
+ text: `Choose a project for ${wizard.selectedDate}.`
1080
+ });
1081
+ }
1082
+ return;
1083
+ }
1084
+
1085
+ if (text === '/clear') {
1086
+ const ok = !wizard.tasks.length || await prompts.confirm('Clear all current tasks?', false);
1087
+ if (!ok) {
1088
+ restoreTaskEditorPanel();
1089
+ return;
1090
+ }
1091
+ await previewDraft({
1092
+ ...wizard,
1093
+ tasks: [],
1094
+ preview: null
1095
+ });
1096
+ return;
1097
+ }
1098
+
1099
+ if (text === '/save') {
1100
+ await saveCurrentDraft();
1101
+ return;
1102
+ }
1103
+
1104
+ if (text === '/drafts') {
1105
+ await openDraftPicker('editor');
1106
+ return;
1107
+ }
1108
+
1109
+ if (text === '/edit') {
1110
+ if (!wizard.tasks.length) {
1111
+ setActivePanel({
1112
+ kind: 'error',
1113
+ title: 'Edit Task',
1114
+ text: 'No tasks to edit.'
1115
+ });
1116
+ return;
1117
+ }
1118
+ setWizard((current) => ({
1119
+ ...current,
1120
+ step: 'edit_task_select',
1121
+ editIndex: 0,
1122
+ replacementError: ''
1123
+ }));
1124
+ setActivePanel({
1125
+ kind: 'logwork',
1126
+ title: 'Edit Task',
1127
+ text: 'Select a task to replace. Esc returns to the editor.'
1128
+ });
1129
+ return;
1130
+ }
1131
+
1132
+ if (text === '/remove') {
1133
+ if (!wizard.tasks.length) {
1134
+ setActivePanel({
1135
+ kind: 'error',
1136
+ title: 'Remove Task',
1137
+ text: 'No tasks to remove.'
1138
+ });
1139
+ return;
1140
+ }
1141
+ setWizard((current) => ({
1142
+ ...current,
1143
+ step: 'remove_tasks',
1144
+ removeIndex: 0,
1145
+ removeSelectedIndexes: []
1146
+ }));
1147
+ setActivePanel({
1148
+ kind: 'logwork',
1149
+ title: 'Remove Tasks',
1150
+ text: 'Select tasks to remove. Space toggles, Enter confirms, Esc cancels.'
1151
+ });
1152
+ return;
1153
+ }
1154
+
1155
+ if (text.startsWith('/remove ')) {
1156
+ const index = Number(text.slice('/remove '.length).trim());
1157
+ if (!Number.isInteger(index)) {
1158
+ setActivePanel({
1159
+ kind: 'error',
1160
+ title: 'Remove Task',
1161
+ text: 'Usage: /remove <task number>'
1162
+ });
1163
+ return;
1164
+ }
1165
+ if (index < 1 || index > wizard.tasks.length) {
1166
+ setActivePanel({
1167
+ kind: 'error',
1168
+ title: 'Remove Task',
1169
+ text: `No task found at index ${index}.`
1170
+ });
1171
+ return;
1172
+ }
1173
+ await previewDraft({
1174
+ ...wizard,
1175
+ tasks: wizard.tasks.filter((_, currentIndex) => currentIndex !== index - 1),
1176
+ preview: null
1177
+ });
1178
+ return;
1179
+ }
1180
+
1181
+ if (text === '/done') {
1182
+ if (!wizard.tasks.length) {
1183
+ setActivePanel({
1184
+ kind: 'error',
1185
+ title: 'Logwork Draft',
1186
+ text: 'Add at least one task before /done.'
1187
+ });
1188
+ return;
1189
+ }
1190
+ await previewDraft(wizard);
1191
+ return;
1192
+ }
1193
+
1194
+ if (text === '/apply') {
1195
+ await applyWizardPreview();
1196
+ return;
1197
+ }
1198
+
1199
+ if (text.startsWith('/')) {
1200
+ setActivePanel({
1201
+ kind: 'error',
1202
+ title: 'Logwork Draft',
1203
+ text: `Unknown logwork command: ${text}`
1204
+ });
1205
+ return;
1206
+ }
1207
+
1208
+ try {
1209
+ const task = parseTaskLine(text);
1210
+ await previewDraft({
1211
+ ...wizard,
1212
+ tasks: [...wizard.tasks, task],
1213
+ preview: null
1214
+ });
1215
+ } catch (error) {
1216
+ setActivePanel({
1217
+ kind: 'error',
1218
+ title: 'Task Error',
1219
+ text: error.message
1220
+ });
1221
+ }
1222
+ }, [
1223
+ applyWizardPreview,
1224
+ cancelCurrentLogwork,
1225
+ executeCommand,
1226
+ openDraftPicker,
1227
+ previewDraft,
1228
+ prompts,
1229
+ restoreTaskEditorPanel,
1230
+ saveCurrentDraft,
1231
+ wizard
1232
+ ]);
1233
+
1234
+ const selectTaskForEdit = useCallback((index) => {
1235
+ const task = wizard.tasks[index];
1236
+ setInputValue('');
1237
+ setWizard((current) => ({
1238
+ ...current,
1239
+ step: 'edit_task_replace',
1240
+ editIndex: index,
1241
+ replacementError: ''
1242
+ }));
1243
+ setActivePanel({
1244
+ kind: 'logwork',
1245
+ title: 'Replace Task',
1246
+ text: [
1247
+ `Replacing ${index + 1}. +${formatTaskHours(task.hours)} ${task.taskName}`,
1248
+ 'Enter replacement line using format: +2 Feature Development | check ui/ux',
1249
+ 'Esc returns without changing the task.'
1250
+ ].join('\n')
1251
+ });
1252
+ }, [wizard]);
1253
+
1254
+ const submitTaskReplacement = useCallback(async (value) => {
1255
+ const text = String(value || '').trim();
1256
+ setInputValue('');
1257
+ if (!text) {
1258
+ setActivePanel({
1259
+ kind: 'error',
1260
+ title: 'Replace Task',
1261
+ text: 'Replacement task is empty. Use format: +2 Feature Development | check ui/ux'
1262
+ });
1263
+ return;
1264
+ }
1265
+ if (text === '/cancel') {
1266
+ setWizard((current) => ({
1267
+ ...current,
1268
+ step: 'edit_tasks',
1269
+ replacementError: ''
1270
+ }));
1271
+ restoreTaskEditorPanel();
1272
+ return;
1273
+ }
1274
+ try {
1275
+ const task = parseTaskLine(text);
1276
+ await previewDraft({
1277
+ ...wizard,
1278
+ step: 'edit_tasks',
1279
+ tasks: replaceDraftTask(wizard.tasks, wizard.editIndex, task),
1280
+ preview: null,
1281
+ replacementError: ''
1282
+ });
1283
+ } catch (error) {
1284
+ setWizard((current) => ({
1285
+ ...current,
1286
+ replacementError: error.message
1287
+ }));
1288
+ setActivePanel({
1289
+ kind: 'error',
1290
+ title: 'Replace Task',
1291
+ text: error.message
1292
+ });
1293
+ }
1294
+ }, [previewDraft, restoreTaskEditorPanel, wizard]);
1295
+
1296
+ const submitInput = useCallback((value) => {
1297
+ const text = String(value || '');
1298
+ const trimmed = text.trim();
1299
+
1300
+ if (pendingConfirm) {
1301
+ return;
1302
+ }
1303
+
1304
+ if (authActive || authPrompt) {
1305
+ return;
1306
+ }
1307
+
1308
+ if (wizard?.step === 'edit_tasks') {
1309
+ const taskSuggestions = getCommandSuggestions(inputValue, TASK_COMMANDS);
1310
+ const selected = taskSuggestions[selectedIndex];
1311
+ if (selected && !inputValue.includes(' ') && trimmed !== selected.name && trimmed !== '') {
1312
+ setInputValue(selected.name);
1313
+ setSelectedIndex(0);
1314
+ return;
1315
+ }
1316
+ submitWizardTaskInput(text);
1317
+ return;
1318
+ }
1319
+
1320
+ if (wizard?.step === 'edit_task_replace') {
1321
+ submitTaskReplacement(text);
1322
+ return;
1323
+ }
1324
+
1325
+ const suggestions = getCommandSuggestions(inputValue);
1326
+ const completed = completeCommandValue(inputValue, suggestions, selectedIndex);
1327
+ if (completed) {
1328
+ setInputValue(completed);
1329
+ setSelectedIndex(0);
1330
+ return;
1331
+ }
1332
+
1333
+ setInputValue('');
1334
+ setSelectedIndex(0);
1335
+
1336
+ if (!trimmed) {
1337
+ return;
1338
+ }
1339
+
1340
+ let command;
1341
+ try {
1342
+ command = controller.parseManualCommand(trimmed);
1343
+ } catch (error) {
1344
+ setActivePanel({
1345
+ kind: 'error',
1346
+ title: 'Command Error',
1347
+ text: error.message
1348
+ });
1349
+ return;
1350
+ }
1351
+
1352
+ if (command.type === 'logwork' && !command.text) {
1353
+ startLogworkWizard(command.target);
1354
+ return;
1355
+ }
1356
+
1357
+ if (command.type === 'auth' && !command.target) {
1358
+ startAuthTargetPicker();
1359
+ return;
1360
+ }
1361
+
1362
+ if (command.type === 'mcp' && !command.client) {
1363
+ startMcpClientPicker();
1364
+ return;
1365
+ }
1366
+
1367
+ if (command.type === 'reconcile' && !command.period) {
1368
+ startReconciliationPeriodPicker();
1369
+ return;
1370
+ }
1371
+
1372
+ executeCommand(command);
1373
+ }, [
1374
+ controller,
1375
+ executeCommand,
1376
+ inputValue,
1377
+ pendingConfirm,
1378
+ authActive,
1379
+ authPrompt,
1380
+ selectedIndex,
1381
+ startMcpClientPicker,
1382
+ startLogworkWizard,
1383
+ startAuthTargetPicker,
1384
+ startReconciliationPeriodPicker,
1385
+ submitTaskReplacement,
1386
+ submitWizardTaskInput,
1387
+ wizard
1388
+ ]);
1389
+
1390
+ return h(Box, {
1391
+ flexDirection: 'column',
1392
+ gap: 1
1393
+ },
1394
+ h(HeaderBar, { cwd }),
1395
+ h(CurrentPanel, { panel: activePanel }),
1396
+ loading ? h(LoadingLine, { message: loading }) : null,
1397
+ h(StatusBar, {
1398
+ preview: activeSessionPreview(sessionRef.current),
1399
+ dryRun: process.env.LOGWORK_DRY_RUN === '1',
1400
+ wizard,
1401
+ authPrompt: authPrompt || (authActive ? { step: 'working' } : null)
1402
+ }),
1403
+ authPrompt
1404
+ ? h(AuthPrompt, {
1405
+ prompt: authPrompt,
1406
+ inputValue,
1407
+ onInputChange(value) {
1408
+ setInputValue(value);
1409
+ },
1410
+ onPromptChange(nextPrompt) {
1411
+ setAuthPrompt(nextPrompt);
1412
+ },
1413
+ onResolve: resolveAuthPrompt,
1414
+ onReject: rejectAuthPrompt
1415
+ })
1416
+ : authActive
1417
+ ? h(AuthPrompt, {
1418
+ prompt: { step: 'working' },
1419
+ inputValue,
1420
+ onInputChange(value) {
1421
+ setInputValue(value);
1422
+ },
1423
+ onPromptChange(nextPrompt) {
1424
+ setAuthPrompt(nextPrompt);
1425
+ },
1426
+ onResolve: resolveAuthPrompt,
1427
+ onReject: rejectAuthPrompt
1428
+ })
1429
+ : pendingConfirm
1430
+ ? h(ConfirmDialog, {
1431
+ message: pendingConfirm.message,
1432
+ initialValue: pendingConfirm.initialValue,
1433
+ onResolve: resolveConfirm
1434
+ })
1435
+ : wizard?.step === 'select_logwork_target'
1436
+ ? h(TargetPicker, {
1437
+ title: 'Pick logwork target',
1438
+ targets: LOGWORK_TARGETS,
1439
+ selectedIndex: wizard.targetIndex || 0,
1440
+ onChange(index) {
1441
+ setWizard((current) => ({
1442
+ ...current,
1443
+ targetIndex: index
1444
+ }));
1445
+ },
1446
+ onSelect(target) {
1447
+ startLogworkWizard(target.key);
1448
+ },
1449
+ onCancel() {
1450
+ setWizard(null);
1451
+ setActivePanel({
1452
+ kind: 'idle',
1453
+ title: 'Ready',
1454
+ text: 'Type / for commands.'
1455
+ });
1456
+ }
1457
+ })
1458
+ : wizard?.step === 'select_auth_target'
1459
+ ? h(TargetPicker, {
1460
+ title: 'Pick auth target',
1461
+ targets: AUTH_TARGETS,
1462
+ selectedIndex: wizard.targetIndex || 0,
1463
+ onChange(index) {
1464
+ setWizard((current) => ({
1465
+ ...current,
1466
+ targetIndex: index
1467
+ }));
1468
+ },
1469
+ onSelect(target) {
1470
+ executeCommand({
1471
+ type: 'auth',
1472
+ target: target.key
1473
+ });
1474
+ },
1475
+ onCancel() {
1476
+ setWizard(null);
1477
+ setActivePanel({
1478
+ kind: 'idle',
1479
+ title: 'Ready',
1480
+ text: 'Type / for commands.'
1481
+ });
1482
+ }
1483
+ })
1484
+ : wizard?.step === 'select_reconciliation_period'
1485
+ ? h(TargetPicker, {
1486
+ title: 'Pick reconciliation period',
1487
+ targets: RECONCILIATION_PERIOD_OPTIONS,
1488
+ selectedIndex: wizard.periodIndex || 0,
1489
+ onChange(index) {
1490
+ setWizard((current) => ({
1491
+ ...current,
1492
+ periodIndex: index
1493
+ }));
1494
+ },
1495
+ onSelect(period) {
1496
+ executeCommand({
1497
+ type: 'reconcile',
1498
+ period: period.key
1499
+ });
1500
+ },
1501
+ onCancel() {
1502
+ setWizard(null);
1503
+ setActivePanel({
1504
+ kind: 'idle',
1505
+ title: 'Ready',
1506
+ text: 'Type / for commands.'
1507
+ });
1508
+ }
1509
+ })
1510
+ : wizard?.step === 'select_draft'
1511
+ ? h(DraftPicker, {
1512
+ drafts: wizard.drafts || [],
1513
+ selectedIndex: wizard.draftIndex || 0,
1514
+ includeStartNew: wizard.draftSource === 'start',
1515
+ onChange(index) {
1516
+ setWizard((current) => ({
1517
+ ...current,
1518
+ draftIndex: index
1519
+ }));
1520
+ },
1521
+ onResume(draft) {
1522
+ resumeManualDraft(draft);
1523
+ },
1524
+ onDelete(draft) {
1525
+ deleteSavedDraft(draft);
1526
+ },
1527
+ onStartNew() {
1528
+ startNewLogworkWizard(RO_TARGET);
1529
+ },
1530
+ onCancel() {
1531
+ returnFromDraftPicker();
1532
+ }
1533
+ })
1534
+ : wizard?.step === 'select_date'
1535
+ ? h(DatePicker, {
1536
+ options: wizard.dateOptions,
1537
+ selectedIndex: wizard.dateIndex,
1538
+ onChange(index) {
1539
+ setWizard((current) => ({
1540
+ ...current,
1541
+ dateIndex: index
1542
+ }));
1543
+ },
1544
+ onSelect(option) {
1545
+ selectWizardDate(option);
1546
+ }
1547
+ })
1548
+ : wizard?.step === 'select_project'
1549
+ ? h(ProjectPicker, {
1550
+ options: wizard.projectOptions,
1551
+ selectedIndex: wizard.projectIndex,
1552
+ onChange(index) {
1553
+ setWizard((current) => ({
1554
+ ...current,
1555
+ projectIndex: index
1556
+ }));
1557
+ },
1558
+ onSelect(project) {
1559
+ selectWizardProject(project);
1560
+ }
1561
+ })
1562
+ : wizard?.step === 'select_mcp_client'
1563
+ ? h(McpClientPicker, {
1564
+ selectedIndex: wizard.mcpClientIndex || 0,
1565
+ onChange(index) {
1566
+ setWizard((current) => ({
1567
+ ...current,
1568
+ mcpClientIndex: index
1569
+ }));
1570
+ },
1571
+ onSelect(client) {
1572
+ executeCommand({
1573
+ type: 'mcp',
1574
+ client: client.key
1575
+ });
1576
+ },
1577
+ onCancel() {
1578
+ cancelMcpClientPicker();
1579
+ }
1580
+ })
1581
+ : wizard?.step === 'remove_tasks'
1582
+ ? h(TaskRemovePicker, {
1583
+ tasks: wizard.tasks,
1584
+ selectedIndex: wizard.removeIndex || 0,
1585
+ selectedIndexes: wizard.removeSelectedIndexes || [],
1586
+ onChange(index) {
1587
+ setWizard((current) => ({
1588
+ ...current,
1589
+ removeIndex: index
1590
+ }));
1591
+ },
1592
+ onToggle(index) {
1593
+ setWizard((current) => ({
1594
+ ...current,
1595
+ removeSelectedIndexes: toggleTaskSelection(current.removeSelectedIndexes || [], index)
1596
+ }));
1597
+ },
1598
+ onSubmit(indexes) {
1599
+ confirmRemoveTasks(indexes);
1600
+ },
1601
+ onCancel() {
1602
+ cancelRemoveTasks();
1603
+ }
1604
+ })
1605
+ : wizard?.step === 'edit_task_select'
1606
+ ? h(TaskEditPicker, {
1607
+ tasks: wizard.tasks,
1608
+ selectedIndex: wizard.editIndex || 0,
1609
+ onChange(index) {
1610
+ setWizard((current) => ({
1611
+ ...current,
1612
+ editIndex: index
1613
+ }));
1614
+ },
1615
+ onSelect(index) {
1616
+ selectTaskForEdit(index);
1617
+ },
1618
+ onCancel() {
1619
+ setWizard((current) => ({
1620
+ ...current,
1621
+ step: 'edit_tasks'
1622
+ }));
1623
+ restoreTaskEditorPanel();
1624
+ }
1625
+ })
1626
+ : h(CommandInput, {
1627
+ value: inputValue,
1628
+ onChange(value) {
1629
+ setInputValue(value);
1630
+ setSelectedIndex(0);
1631
+ },
1632
+ onSubmit: submitInput,
1633
+ selectedIndex,
1634
+ setSelectedIndex,
1635
+ mode: wizard?.step === 'edit_tasks' ? 'task' : wizard?.step === 'edit_task_replace' ? 'task-edit' : 'command',
1636
+ onEscape() {
1637
+ if (!wizard) {
1638
+ exit();
1639
+ return;
1640
+ }
1641
+ if (wizard?.step === 'edit_tasks') {
1642
+ cancelCurrentLogwork();
1643
+ return;
1644
+ }
1645
+ if (wizard?.step === 'edit_task_replace') {
1646
+ setWizard((current) => ({
1647
+ ...current,
1648
+ step: 'edit_tasks'
1649
+ }));
1650
+ restoreTaskEditorPanel();
1651
+ }
1652
+ }
1653
+ }));
1654
+ }