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.
- package/LICENSE +11 -0
- package/README.md +287 -2
- package/RELEASE.md +89 -0
- package/bin/logwork.mjs +101 -0
- package/commands/auth.mjs +227 -0
- package/commands/delete.mjs +69 -0
- package/commands/doctor.mjs +24 -0
- package/commands/edit.mjs +88 -0
- package/commands/history.mjs +62 -0
- package/commands/query.mjs +52 -0
- package/commands/quick.mjs +163 -0
- package/commands/reconcile.mjs +55 -0
- package/commands/release.mjs +77 -0
- package/commands/reminder.mjs +172 -0
- package/commands/setup.mjs +504 -0
- package/commands/tui.mjs +50 -0
- package/commands/update.mjs +161 -0
- package/docs/advanced.md +334 -0
- package/docs/mcp-setup.md +347 -0
- package/docs/operations-guide.md +587 -0
- package/docs/recipes.md +259 -0
- package/docs/security.md +151 -0
- package/examples/mcp/antigravity.mcp_config.json +8 -0
- package/examples/mcp/codex.config.toml +5 -0
- package/examples/mcp/copilot.mcp.json +9 -0
- package/lib/api-auth.mjs +1259 -0
- package/lib/api.mjs +1422 -0
- package/lib/apply-ledger.mjs +227 -0
- package/lib/atomic-file.mjs +24 -0
- package/lib/auth-diagnostics.mjs +113 -0
- package/lib/auth-errors.mjs +22 -0
- package/lib/auth-redaction.mjs +78 -0
- package/lib/auth-terminal.mjs +97 -0
- package/lib/auth.mjs +323 -0
- package/lib/batch-parser.mjs +145 -0
- package/lib/batch-workflow.mjs +842 -0
- package/lib/config.mjs +191 -0
- package/lib/credential-store.mjs +130 -0
- package/lib/diagnostics.mjs +307 -0
- package/lib/entrypoint.mjs +14 -0
- package/lib/file-lock.mjs +44 -0
- package/lib/http.mjs +125 -0
- package/lib/jira-api.mjs +258 -0
- package/lib/jira-auth.mjs +203 -0
- package/lib/jira-workflow.mjs +523 -0
- package/lib/keychain.mjs +105 -0
- package/lib/legacy-migration.mjs +66 -0
- package/lib/list-navigation.mjs +19 -0
- package/lib/logwork-config.mjs +199 -0
- package/lib/logwork-f03-enrich.mjs +41 -0
- package/lib/macos.mjs +33 -0
- package/lib/manual-apply-state.mjs +35 -0
- package/lib/manual-commands.mjs +294 -0
- package/lib/manual-drafts.mjs +154 -0
- package/lib/manual-ink-app.mjs +1654 -0
- package/lib/manual-ink-auth.mjs +349 -0
- package/lib/manual-ink-ui.mjs +675 -0
- package/lib/manual-input.mjs +21 -0
- package/lib/manual-logwork-wizard.mjs +310 -0
- package/lib/manual-mcp.mjs +187 -0
- package/lib/manual-repl.mjs +1037 -0
- package/lib/manual-targets.mjs +78 -0
- package/lib/mcp-server.mjs +793 -0
- package/lib/os-notification.mjs +105 -0
- package/lib/package-info.mjs +17 -0
- package/lib/paths.mjs +68 -0
- package/lib/project-identity.mjs +66 -0
- package/lib/project-mapping-workflow.mjs +309 -0
- package/lib/project-resolver.mjs +373 -0
- package/lib/query-workflow.mjs +553 -0
- package/lib/reconciliation-workflow.mjs +446 -0
- package/lib/release-doctor.mjs +448 -0
- package/lib/reminder-config.mjs +71 -0
- package/lib/reminder-scheduler.mjs +301 -0
- package/lib/reminder-service.mjs +118 -0
- package/lib/reminder-state.mjs +38 -0
- package/lib/reminder-workflow.mjs +299 -0
- package/lib/ro-delete-args.mjs +38 -0
- package/lib/ro-delete-workflow.mjs +115 -0
- package/lib/ro-edit-args.mjs +142 -0
- package/lib/ro-edit-workflow.mjs +632 -0
- package/lib/ro-entry-reader.mjs +56 -0
- package/lib/ro-resubmit-workflow.mjs +134 -0
- package/lib/terminal-auth-prompts.mjs +120 -0
- package/lib/ui.mjs +133 -0
- package/lib/update-service.mjs +350 -0
- package/lib/util.mjs +66 -0
- package/lib/windows-credential-manager.mjs +260 -0
- package/lib/worklog-task-resolver.mjs +113 -0
- package/package.json +72 -4
- 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
|
+
}
|