@harness-mix/cli 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +469 -467
- package/output/native-build/desktop-controller.mjs +1 -1
- package/output/native-build/renderer-extension.js +23 -4
- package/package.json +11 -9
- package/scripts/antigravity-adapter-test.cjs +647 -626
- package/scripts/codex-adapter-test.cjs +162 -127
- package/scripts/collaboration-test.cjs +274 -262
- package/scripts/jsonl-stdin-test.cjs +40 -31
- package/scripts/kiro-cursor-adapters-test.cjs +124 -100
- package/scripts/native-acp-depth-test.cjs +30 -5
- package/scripts/native-update-apply-test.cjs +269 -215
- package/scripts/native-update.cjs +78 -0
- package/scripts/native-vendor-adapters-test.cjs +196 -154
- package/scripts/salvage-rollout-writes.cjs +72 -0
- package/scripts/zcode-adapter-test.cjs +329 -0
- package/scripts/zcode-live-probe.cjs +66 -0
- package/src/main/adapters/antigravity.js +1428 -1418
- package/src/main/adapters/codex.js +656 -649
- package/src/main/adapters/native-acp-command.js +51 -48
- package/src/main/adapters/native-acp.js +47 -12
- package/src/main/adapters/qoder.js +12 -8
- package/src/main/adapters/zcode.js +921 -10
- package/src/main/host/collaboration.js +723 -715
- package/src/main/host/jsonl.js +130 -120
- package/src/main/native/config.js +9 -9
- package/src/main/native/launcher.js +252 -237
- package/src/main/native/process-utils.js +157 -57
- package/src/main/native/protocol.js +1221 -1187
- package/src/main/native/update-state.js +123 -110
- package/src/main/native/updater.js +460 -394
- package/src/native-ui/desktop-control/src/renderer-cdp-control-session.ts +358 -358
- package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +3211 -3181
- package/src/native-ui/renderer-extension/src/settings/connections-page.ts +2 -2
|
@@ -1,1187 +1,1221 @@
|
|
|
1
|
-
// Projection boundary only. Execution, persistence and native approvals belong
|
|
2
|
-
// to Harness Mix HostRuntime / ProtocolCore and their existing adapters.
|
|
3
|
-
const { getHarnessSvg } = require('./icons');
|
|
4
|
-
const { prepareInput } = require('./input');
|
|
5
|
-
const { projectUsage, projectAccountCredits } = require('./usage');
|
|
6
|
-
const { exec } = require('node:child_process');
|
|
7
|
-
const os = require('node:os');
|
|
8
|
-
const fs = require('node:fs');
|
|
9
|
-
const path = require('node:path');
|
|
10
|
-
const { randomUUID } = require('node:crypto');
|
|
11
|
-
const { diff } = require('../workspace/diff');
|
|
12
|
-
const {
|
|
13
|
-
fetchLatestVersion,
|
|
14
|
-
remoteState,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
'
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
'
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
'
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
'
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (parts.length
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (model
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (typeof change.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// thread/start 用
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (typeof params.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (typeof
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (phase === '
|
|
209
|
-
return
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (item.type === '
|
|
222
|
-
if (item.type === '
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (item.type === '
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
this.
|
|
271
|
-
this.
|
|
272
|
-
this.
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
this.
|
|
279
|
-
this.
|
|
280
|
-
this.
|
|
281
|
-
this.
|
|
282
|
-
this.
|
|
283
|
-
this.
|
|
284
|
-
this.
|
|
285
|
-
this.
|
|
286
|
-
this.
|
|
287
|
-
this.
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (event?.type === 'thread-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
this.
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
//
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
if (!this.runtime.
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
//
|
|
391
|
-
|
|
392
|
-
const
|
|
393
|
-
const
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
if (
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
if (
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
if (method === 'harnessmix/
|
|
456
|
-
if (method === 'harnessmix/
|
|
457
|
-
|
|
458
|
-
if (method === 'harnessmix/
|
|
459
|
-
if (method === 'harnessmix/
|
|
460
|
-
if (method === 'harnessmix/
|
|
461
|
-
if (method === 'harnessmix/
|
|
462
|
-
if (method === 'harnessmix/
|
|
463
|
-
|
|
464
|
-
if (method === 'harnessmix/
|
|
465
|
-
if (method === 'harnessmix/
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
if (method === 'harnessmix/
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
if (method === 'harnessmix/
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
//
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
if (
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
const
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
if (method === 'thread/
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
if (
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
//
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
if (method === '
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
const
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
}
|
|
970
|
-
if (method === '
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
}
|
|
979
|
-
return
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
if (method === 'harnessmix/thread/
|
|
983
|
-
const
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
if (
|
|
1006
|
-
if (
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
if (event.type.
|
|
1054
|
-
|
|
1055
|
-
notify('
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
if (
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
if (
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
const
|
|
1155
|
-
//
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
if (
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1
|
+
// Projection boundary only. Execution, persistence and native approvals belong
|
|
2
|
+
// to Harness Mix HostRuntime / ProtocolCore and their existing adapters.
|
|
3
|
+
const { getHarnessSvg } = require('./icons');
|
|
4
|
+
const { prepareInput } = require('./input');
|
|
5
|
+
const { projectUsage, projectAccountCredits } = require('./usage');
|
|
6
|
+
const { exec, spawn } = require('node:child_process');
|
|
7
|
+
const os = require('node:os');
|
|
8
|
+
const fs = require('node:fs');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
const { randomUUID } = require('node:crypto');
|
|
11
|
+
const { diff } = require('../workspace/diff');
|
|
12
|
+
const {
|
|
13
|
+
fetchLatestVersion,
|
|
14
|
+
remoteState,
|
|
15
|
+
makeGit,
|
|
16
|
+
asyncRun,
|
|
17
|
+
resolveRegistry,
|
|
18
|
+
} = require('./updater');
|
|
19
|
+
const {
|
|
20
|
+
detectChannel,
|
|
21
|
+
compareVersions,
|
|
22
|
+
readState,
|
|
23
|
+
} = require('./update-state');
|
|
24
|
+
const { nativeEnvironment } = require('./config');
|
|
25
|
+
const { CodexAccountManager } = require('./codex-accounts');
|
|
26
|
+
|
|
27
|
+
const REPO_ROOT = path.resolve(__dirname, '../../..');
|
|
28
|
+
const PACKAGE_JSON_PATH = path.join(REPO_ROOT, 'package.json');
|
|
29
|
+
|
|
30
|
+
const ALIASES = { workbuddy: 'codebuddy', 'claude-code': 'claude', 'deepseek-harness': 'dsh', 'codex-harness': 'codex' };
|
|
31
|
+
const externalId = id => ({ workbuddy: 'codebuddy', claude: 'claude-code', dsh: 'deepseek-harness', codex: 'codex-harness' }[id] || id);
|
|
32
|
+
const { errorActions } = require('../harness-adapter/error-kind');
|
|
33
|
+
const modelRef = model => ({ id: Buffer.from(JSON.stringify({ id: model.id, provider: model.provider })).toString('base64url') });
|
|
34
|
+
const routeModel = harnessId => ['pi', 'claude-code', 'deepseek-harness', 'antigravity', 'omp', 'opencode', 'grok'].includes(harnessId)
|
|
35
|
+
? `harnessmix/${harnessId}-native`
|
|
36
|
+
: `harnessmix/plugin-v1@${Buffer.from(JSON.stringify({ harnessId })).toString('hex')}`;
|
|
37
|
+
const terminal = status => ['completed', 'cancelled', 'error', 'failed'].includes(status);
|
|
38
|
+
const turnStatus = status => ({ cancelled: 'interrupted', error: 'failed', failed: 'failed', completed: 'completed' }[status] || 'inProgress');
|
|
39
|
+
const HARNESS_INSTALL_COMMANDS = {
|
|
40
|
+
qoder: { win32: 'npm install -g @qoder-ai/qodercli', default: 'npm install -g @qoder-ai/qodercli' },
|
|
41
|
+
codex: { win32: 'npm install -g @openai/codex', default: 'npm install -g @openai/codex' },
|
|
42
|
+
'codex-harness': { win32: 'npm install -g @openai/codex', default: 'npm install -g @openai/codex' },
|
|
43
|
+
pi: { win32: 'npm install -g @mariozechner/pi-coding-agent', default: 'npm install -g @mariozechner/pi-coding-agent' },
|
|
44
|
+
'claude-code': { win32: 'npm install -g @anthropic-ai/claude-code', default: 'npm install -g @anthropic-ai/claude-code' },
|
|
45
|
+
'deepseek-harness': { win32: 'pip install deepseek-harness', default: 'pip3 install deepseek-harness' },
|
|
46
|
+
opencode: { win32: 'npm install -g opencode-ai', default: 'npm install -g opencode-ai' },
|
|
47
|
+
grok: { win32: 'powershell -NoProfile -ExecutionPolicy Bypass -Command iex (irm https://x.ai/cli/install.ps1)', default: 'curl -fsSL https://x.ai/cli/install.sh | bash' },
|
|
48
|
+
omp: { win32: 'npm install -g @oh-my-pi/pi-coding-agent', default: 'npm install -g @oh-my-pi/pi-coding-agent' },
|
|
49
|
+
antigravity: { win32: 'powershell -NoProfile -ExecutionPolicy Bypass -Command iex (irm https://antigravity.google/cli/install.ps1)', default: 'curl -fsSL https://antigravity.google/cli/install.sh | bash' },
|
|
50
|
+
openclaw: { win32: 'npm install -g openclaw', default: 'npm install -g openclaw' },
|
|
51
|
+
hermes: { win32: 'pip install hermes-agent', default: 'pip3 install hermes-agent' },
|
|
52
|
+
cline: { win32: 'npm install -g cline', default: 'npm install -g cline' },
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const HARNESS_AUTH_INFO = {
|
|
56
|
+
codex: { name: 'Codex', plan: 'OpenAI / Codex Desktop', label: 'Codex Desktop 内置登录', loginCommand: 'codex login', configHint: 'Codex 官方桌面客户端内置登录状态' },
|
|
57
|
+
'claude-code': { name: 'Claude Code', plan: 'Anthropic Claude', label: 'Claude Code 授权', loginCommand: 'claude login', configHint: '~/.claude.json 或环境变量 ANTHROPIC_API_KEY' },
|
|
58
|
+
'deepseek-harness': { name: 'DeepSeek Harness', plan: 'DeepSeek API', label: 'DeepSeek API Key', loginCommand: null, configHint: '环境变量 DEEPSEEK_API_KEY' },
|
|
59
|
+
antigravity: { name: 'Antigravity CLI', plan: 'Google Gemini', label: 'Google Cloud ADC 认证', loginCommand: 'gcloud auth application-default login', configHint: 'Google Application Default Credentials (ADC) 或 gcloud 认证' },
|
|
60
|
+
pi: { name: 'Pi', plan: 'Pi Multi-Provider', label: 'Pi 模型凭据配置', loginCommand: null, configHint: '~/.pi/agent/settings.json 或各模型提供商 API Key' },
|
|
61
|
+
omp: { name: 'Oh My Pi', plan: 'Oh My Pi', label: 'OMP 模型凭据配置', loginCommand: null, configHint: '~/.omp/ 配置文件或各 Provider API 密钥' },
|
|
62
|
+
grok: { name: 'Grok', plan: 'xAI Grok', label: 'xAI API Key', loginCommand: null, configHint: '环境变量 XAI_API_KEY' },
|
|
63
|
+
opencode: { name: 'OpenCode', plan: 'OpenCode Providers', label: 'OpenCode Provider 配置', loginCommand: null, configHint: '~/.opencode/ 配置文件' },
|
|
64
|
+
openclaw: { name: 'OpenClaw', plan: 'OpenClaw Gateway', label: 'OpenClaw Token 认证', loginCommand: null, configHint: '环境变量 OPENCLAW_TOKEN 或 gateway.auth 配置' },
|
|
65
|
+
'kiro-cli': { name: 'Kiro', plan: 'AWS / Kiro CLI', label: 'Kiro CLI 授权', loginCommand: 'kiro auth login', configHint: '命令行 kiro auth login 或 ~/.kiro/ 目录' },
|
|
66
|
+
'cursor-cli': { name: 'Cursor', plan: 'Cursor Account', label: 'Cursor 账号认证', loginCommand: 'cursor-cli auth login', configHint: 'Cursor 客户端或 cursor-cli auth 登录' },
|
|
67
|
+
qoder: { name: 'Qoder', plan: 'Qoder AI', label: 'Qoder CLI 认证', loginCommand: 'qoder auth login', configHint: '命令行 qoder auth login 或 ~/.qoder/ 配置' },
|
|
68
|
+
codebuddy: { name: 'CodeBuddy', plan: 'CodeBuddy AI', label: 'CodeBuddy 凭据', loginCommand: null, configHint: 'CodeBuddy 客户端或环境变量配置' },
|
|
69
|
+
hermes: { name: 'Hermes', plan: 'Nous Hermes', label: 'Hermes Agent 配置', loginCommand: null, configHint: '~/.hermes/ 配置文件或各模型 API Key' },
|
|
70
|
+
zcode: { name: 'ZCode', plan: 'ZCode AI', label: 'ZCode 账号与配置', loginCommand: null, configHint: 'ZCode 客户端或配置文件' },
|
|
71
|
+
trae: { name: 'Trae', plan: 'Trae AI', label: 'Trae 账号与配置', loginCommand: null, configHint: 'Trae 客户端登录状态' },
|
|
72
|
+
cline: { name: 'Cline', plan: 'Cline Providers', label: 'Cline CLI 认证', loginCommand: 'cline auth', configHint: '命令行 cline auth 或 ~/.cline/data 配置' },
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const MODEL_REF_ID = /^[A-Za-z0-9._~-]{1,512}$/;
|
|
76
|
+
// Positional suffix contract of the versioned Renderer adapter transport ids.
|
|
77
|
+
// Mirrors transportModelId()/decode*TransportModelId() in
|
|
78
|
+
// src/native-ui/renderer-extension/src/versioned-renderer-adapter.ts exactly.
|
|
79
|
+
const LEGACY_SUFFIX_FIELDS = {
|
|
80
|
+
pi: ['model', 'thinkingOptionId'],
|
|
81
|
+
'claude-code': ['model', 'permissionModeId', 'thinkingOptionId'],
|
|
82
|
+
'deepseek-harness': ['model', 'permissionModeId'],
|
|
83
|
+
antigravity: ['model', 'permissionModeId', 'thinkingOptionId'],
|
|
84
|
+
opencode: ['model', 'permissionModeId', 'thinkingOptionId'],
|
|
85
|
+
grok: ['model', 'permissionModeId', 'thinkingOptionId'],
|
|
86
|
+
// OMP(Pi 家族):两段式是 model@thinking,三段式才是 model@permission@thinking
|
|
87
|
+
omp: ['model', 'thinkingOptionId'],
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
function decodeLegacySuffix(harnessId, suffix) {
|
|
91
|
+
const fields = LEGACY_SUFFIX_FIELDS[harnessId];
|
|
92
|
+
const parts = suffix.split('@');
|
|
93
|
+
if (parts.length > 3) throw new Error('Invalid native Harness route');
|
|
94
|
+
// OMP 特例:三段式 model@permission@thinking(中间槽可为空),两段式 model@thinking
|
|
95
|
+
if (harnessId === 'omp' && parts.length === 3) return decodePositional(harnessId, ['model', 'permissionModeId', 'thinkingOptionId'], parts);
|
|
96
|
+
if (parts.length > fields.length) throw new Error('Invalid native Harness route');
|
|
97
|
+
return decodePositional(harnessId, fields, parts);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function decodePositional(harnessId, fields, parts) {
|
|
101
|
+
const route = { harnessId };
|
|
102
|
+
parts.forEach((part, index) => {
|
|
103
|
+
// Optional middle slots may be empty (e.g. model@@thinkingOption).
|
|
104
|
+
if (!part) return;
|
|
105
|
+
const field = fields[index];
|
|
106
|
+
if (field === 'model') {
|
|
107
|
+
if (!MODEL_REF_ID.test(part)) throw new Error('Invalid native Harness route');
|
|
108
|
+
route.model = { id: part };
|
|
109
|
+
} else {
|
|
110
|
+
route[field] = part;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
return route;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function decodeRoute(model) {
|
|
117
|
+
if (typeof model !== 'string') return null;
|
|
118
|
+
if (model.startsWith('harnessmix/plugin-v1@')) {
|
|
119
|
+
const hex = model.slice('harnessmix/plugin-v1@'.length);
|
|
120
|
+
if (model.length > 4096 || !/^(?:[a-f0-9]{2})+$/.test(hex)) throw new Error('Invalid native Harness route');
|
|
121
|
+
const route = JSON.parse(Buffer.from(hex, 'hex').toString());
|
|
122
|
+
if (!route.harnessId || route.harnessId === 'codex') throw new Error('Invalid external Harness');
|
|
123
|
+
return route;
|
|
124
|
+
}
|
|
125
|
+
const legacy = /^harnessmix\/(pi|claude-code|deepseek-harness|antigravity|omp|opencode|grok)-native(?:@(.+))?$/.exec(model);
|
|
126
|
+
if (!legacy) return null;
|
|
127
|
+
return legacy[2] === undefined ? { harnessId: legacy[1] } : decodeLegacySuffix(legacy[1], legacy[2]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// file_change 的 diff 载体有三种形态:字符串 unified diff(codex 原生)、
|
|
131
|
+
// workspace/diff 的结果对象 {rows}(canonicalChanges/审查快照)、或裸 before/after。
|
|
132
|
+
// 统一规范化为完整 git 风格 unified diff:Desktop 以 `diff --git a/x b/x` 头切分文件并
|
|
133
|
+
// 提取干净路径,且只在 `@@` hunk 头之后才开始计数增删行——两者缺失都会显示 +0 -0。
|
|
134
|
+
function unifiedBody(change) {
|
|
135
|
+
if (typeof change.diff === 'string' && change.diff.trim()) return change.diff;
|
|
136
|
+
if (typeof change.patch === 'string' && change.patch.trim()) return change.patch;
|
|
137
|
+
const rows = Array.isArray(change.patch?.rows) ? change.patch.rows
|
|
138
|
+
: (typeof change.before === 'string' || typeof change.after === 'string') ? diff(change.before || '', change.after || '').rows
|
|
139
|
+
: null;
|
|
140
|
+
if (!rows?.length) return '';
|
|
141
|
+
const removed = rows.filter(row => row.kind !== 'add').length;
|
|
142
|
+
const added = rows.filter(row => row.kind !== 'remove').length;
|
|
143
|
+
const hunk = `@@ -${removed ? `1,${removed}` : '0,0'} +${added ? `1,${added}` : '0,0'} @@`;
|
|
144
|
+
return [hunk, ...rows.map(row => (row.kind === 'add' ? '+' : row.kind === 'remove' ? '-' : ' ') + row.text)].join('\n');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function unifiedDiff(change, kindType) {
|
|
148
|
+
const body = unifiedBody(change);
|
|
149
|
+
if (/^diff --git /m.test(body)) return body; // 已是完整 unified diff,原样透传
|
|
150
|
+
const file = change.path || change.file || '';
|
|
151
|
+
const headers = kindType === 'add' ? ['--- /dev/null', `+++ b/${file}`]
|
|
152
|
+
: kindType === 'delete' ? [`--- a/${file}`, '+++ /dev/null']
|
|
153
|
+
: [`--- a/${file}`, `+++ b/${file}`];
|
|
154
|
+
return [`diff --git a/${file} b/${file}`, ...(/^---\s/m.test(body) ? [] : headers), body].filter(Boolean).join('\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 各 harness 终端工具的命名集合;命中即把该工具调用投影为原生 commandExecution,
|
|
158
|
+
// 让 Desktop 显示「正在运行/运行了命令」而不是笼统的「已使用 Harness Mix 集成」。
|
|
159
|
+
const SHELL_TOOL_TITLE = /bash|shell|terminal|console|exec|powershell|pwsh|\bcmd\b|command|命令|终端/i;
|
|
160
|
+
|
|
161
|
+
function shellCommandText(item) {
|
|
162
|
+
if (!SHELL_TOOL_TITLE.test(String(item.title || ''))) return null;
|
|
163
|
+
const input = item.input;
|
|
164
|
+
if (typeof input !== 'string' || !input.trim()) return String(item.title || 'command');
|
|
165
|
+
try {
|
|
166
|
+
const parsed = JSON.parse(input);
|
|
167
|
+
if (parsed && typeof parsed === 'object') {
|
|
168
|
+
const cmd = parsed.command ?? parsed.cmd ?? parsed.script ?? parsed.input;
|
|
169
|
+
if (typeof cmd === 'string' && cmd.trim()) return cmd;
|
|
170
|
+
}
|
|
171
|
+
} catch {}
|
|
172
|
+
return input;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 官方 app-server 由 Rust 把命令解析成 commandActions;这里只对无引号/管道/重定向的
|
|
176
|
+
// 简单单命令做保守分类(读取/搜索/列目录),其余一律 unknown,不臆造命令行为。
|
|
177
|
+
function classifyCommandAction(command) {
|
|
178
|
+
if (/["'`|;&<>()[\]{}$]/.test(command)) return { type: 'unknown', command };
|
|
179
|
+
const tokens = command.trim().split(/\s+/).filter(Boolean);
|
|
180
|
+
const exe = (tokens[0] || '').toLowerCase().replace(/^.*[\\/]/, '').replace(/\.exe$/, '');
|
|
181
|
+
const args = tokens.slice(1).filter(t => !t.startsWith('-'));
|
|
182
|
+
if (['cat', 'head', 'tail', 'type', 'get-content', 'more'].includes(exe) && args.length === 1)
|
|
183
|
+
return { type: 'read', command, name: args[0].split(/[\\/]/).pop(), path: args[0] };
|
|
184
|
+
if (['rg', 'grep', 'egrep', 'fgrep', 'findstr'].includes(exe) && args.length >= 1)
|
|
185
|
+
return { type: 'search', command, query: args[0] };
|
|
186
|
+
if (['ls', 'dir', 'tree'].includes(exe))
|
|
187
|
+
return { type: 'listFiles', command, ...(args[0] ? { path: args[0] } : {}) };
|
|
188
|
+
return { type: 'unknown', command };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Desktop 权限菜单/回合参数中的权限字段(thread/settings/update 与 turn/start 用 sandboxPolicy,
|
|
192
|
+
// thread/start 用 sandbox),统一存为 turnPermissions { approvalPolicy, approvalsReviewer, sandboxPolicy, permissions }
|
|
193
|
+
function pickTurnPermissions(params = {}) {
|
|
194
|
+
const picked = {};
|
|
195
|
+
if (typeof params.approvalPolicy === 'string') picked.approvalPolicy = params.approvalPolicy;
|
|
196
|
+
if (typeof params.approvalsReviewer === 'string') picked.approvalsReviewer = params.approvalsReviewer;
|
|
197
|
+
const sandbox = params.sandboxPolicy ?? params.sandbox;
|
|
198
|
+
if (sandbox && typeof sandbox === 'object') picked.sandboxPolicy = sandbox;
|
|
199
|
+
if (typeof params.permissions === 'string') picked.permissions = params.permissions;
|
|
200
|
+
return Object.keys(picked).length ? picked : null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Desktop 渲染层按字面量匹配 agentMessage 的 phase 词表(commentary / final_answer):
|
|
204
|
+
// split-items-into-render-groups 只在 phase==='final_answer' 时把末段提取为回合结论,
|
|
205
|
+
// 其余 assistant-message 随执行区折叠进「用时」栏。内部 CoreEvent 词表是 progress / final,
|
|
206
|
+
// 在此 Codex 投影边界翻译;历史持久化数据(progress/final)也经此映射,无需迁移。
|
|
207
|
+
function agentMessagePhase(phase) {
|
|
208
|
+
if (phase === 'progress' || phase === 'commentary') return 'commentary';
|
|
209
|
+
if (phase === 'final' || phase === 'final_answer' || phase == null) return 'final_answer';
|
|
210
|
+
return phase;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function projectItem(item) {
|
|
214
|
+
const base = { id: item.id };
|
|
215
|
+
if (item.type === 'user_message') return { ...base, type: 'userMessage', content: [
|
|
216
|
+
{ type: 'text', text: item.content || '', text_elements: [] },
|
|
217
|
+
...(item.attachments || []).filter(a => a.kind === 'image').map(a => a.data
|
|
218
|
+
? { type: 'image', url: `data:${a.mime || 'image/png'};base64,${a.data}` }
|
|
219
|
+
: a.path ? { type: 'localImage', path: a.path } : { type: 'text', text: `[图片:${a.name}]`, text_elements: [] }),
|
|
220
|
+
] };
|
|
221
|
+
if (item.type === 'agent_message' || item.type === 'notice') return { ...base, type: 'agentMessage', text: item.content || '', phase: agentMessagePhase(item.phase) };
|
|
222
|
+
if (item.type === 'reasoning') return { ...base, type: 'reasoning', summary: [item.content || ''], content: [] };
|
|
223
|
+
if (item.type === 'tool_call' && item.collaboration) {
|
|
224
|
+
const job = item.collaboration;
|
|
225
|
+
const status = ({ completed: 'completed', failed: 'errored', cancelled: 'interrupted', interrupted: 'interrupted' })[job.status] || 'running';
|
|
226
|
+
return { ...base, type: 'collabAgentToolCall', tool: job.operation || 'spawnAgent',
|
|
227
|
+
status: terminal(item.status) ? (item.state === 'error' ? 'failed' : 'completed') : 'inProgress',
|
|
228
|
+
senderThreadId: job.parent_thread_id, receiverThreadIds: job.child_thread_id ? [job.child_thread_id] : [],
|
|
229
|
+
prompt: job.task || null, model: null, reasoningEffort: null,
|
|
230
|
+
childThreadId: job.child_thread_id || null, agentType: job.agent_type || null,
|
|
231
|
+
diff: job.diff || null, digest: job.digest || null,
|
|
232
|
+
branch: job.branch || job.workspace?.branch || null,
|
|
233
|
+
workspaceMode: job.workspace?.mode || 'shared',
|
|
234
|
+
applied: !!job.appliedDigest || !!job.applied,
|
|
235
|
+
agentsStates: job.child_thread_id ? { [job.child_thread_id]: { status,
|
|
236
|
+
message: job.attention?.message || job.attention?.title || job.result || job.error || null } } : {} };
|
|
237
|
+
}
|
|
238
|
+
if (item.type === 'tool_call') {
|
|
239
|
+
const status = terminal(item.status) ? (item.state === 'error' ? 'failed' : 'completed') : 'inProgress';
|
|
240
|
+
const command = shellCommandText(item);
|
|
241
|
+
if (command != null) {
|
|
242
|
+
// 退出码未知不臆造(exitCode: null);durationMs 来自 Core 真实时间戳
|
|
243
|
+
return { ...base, type: 'commandExecution', command, cwd: null, status,
|
|
244
|
+
commandActions: [classifyCommandAction(command)],
|
|
245
|
+
aggregatedOutput: item.output ? String(item.output) : null,
|
|
246
|
+
exitCode: null, durationMs: terminal(item.status) ? Math.max(0, (item.updatedAt ?? 0) - (item.createdAt ?? 0)) : null, processId: null };
|
|
247
|
+
}
|
|
248
|
+
return { ...base, type: 'dynamicToolCall', namespace: 'harness-mix', tool: item.title || 'tool',
|
|
249
|
+
arguments: item.input || {}, contentItems: item.output ? [{ type: 'inputText', text: String(item.output) }] : null,
|
|
250
|
+
status, success: terminal(item.status) ? item.state !== 'error' : null };
|
|
251
|
+
}
|
|
252
|
+
if (item.type === 'file_change') return { ...base, type: 'fileChange', status: terminal(item.status) ? 'completed' : 'inProgress',
|
|
253
|
+
changes: (item.changes || [item]).map(change => {
|
|
254
|
+
const kind = change.changeType === 'deleted' ? { type: 'delete' } : change.changeType === 'added' ? { type: 'add' } : { type: 'update', move_path: null };
|
|
255
|
+
return { path: change.path || change.file || '', kind, diff: unifiedDiff(change, kind.type) };
|
|
256
|
+
}) };
|
|
257
|
+
if (item.type === 'context_compaction' || item.type === 'contextCompaction') return { ...base, type: 'contextCompaction' };
|
|
258
|
+
if (item.type === 'verification_report') {
|
|
259
|
+
const report = item.report ?? {};
|
|
260
|
+
const failed = report.status === 'failed';
|
|
261
|
+
return { ...base, type: 'mcpToolCall', server: 'harness-mix', tool: 'verification_gate',
|
|
262
|
+
arguments: { mode: report.mode ?? 'off' }, result: { content: [{ type: 'text', text: JSON.stringify(report) }], structuredContent: report },
|
|
263
|
+
status: failed ? 'failed' : 'completed', error: failed ? { message: 'Verification gate failed' } : null, durationMs: Math.max(0, (report.completedAt ?? 0) - (report.startedAt ?? 0)) };
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
class NativeProtocol {
|
|
269
|
+
constructor(runtime, emit, requestOfficial = null) {
|
|
270
|
+
this.runtime = runtime;
|
|
271
|
+
this.emit = emit;
|
|
272
|
+
this.requestOfficial = requestOfficial;
|
|
273
|
+
this.codexAccounts = new CodexAccountManager({
|
|
274
|
+
dataDirectory: nativeEnvironment().HARNESSMIX_DATA_DIR,
|
|
275
|
+
requestOfficial,
|
|
276
|
+
emit,
|
|
277
|
+
});
|
|
278
|
+
this.approvals = new Map();
|
|
279
|
+
this.pets = require('./pets').createPetMarket();
|
|
280
|
+
this.published = new Map();
|
|
281
|
+
this.steering = new Map(); // threadId -> in-flight steer promise
|
|
282
|
+
this.steerReceipts = new Map(); // `${threadId}\0${clientUserMessageId}` -> bounded delivery receipt
|
|
283
|
+
this.queues = new Map(); // threadId -> Array<{ id, input, clientUserMessageId, createdAt }>
|
|
284
|
+
this.queueNotifications = new Map(); // response-first queue notifications, coalesced per thread
|
|
285
|
+
this.queueStarts = new Set(); // queued submission ids currently being accepted by a native harness
|
|
286
|
+
this.closed = false;
|
|
287
|
+
this.turnDiffs = new Map(); // `${threadId}\0${turnId}` -> last pushed diff text (dedupe)
|
|
288
|
+
this.unsubscribe = runtime.core.subscribe(({ event, projected }) => this.onCore(event, projected));
|
|
289
|
+
// Host 侧新建的线程(协作子任务等)也要通知 Desktop 侧栏,与 thread/start 同一契约
|
|
290
|
+
this.unsubscribeRuntime = runtime.subscribe(event => {
|
|
291
|
+
if (event?.type === 'thread-created' && event.thread) this.emit({ method: 'thread/started', params: { thread: this.projectThread(event.thread) } });
|
|
292
|
+
if (event?.type === 'thread-updated' && event.thread) {
|
|
293
|
+
this.emit({ method: 'thread/name/updated', params: { threadId: event.thread.id, threadName: event.thread.title } });
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
getQueue(threadId) {
|
|
298
|
+
if (!this.queues.has(threadId)) this.queues.set(threadId, []);
|
|
299
|
+
return this.queues.get(threadId);
|
|
300
|
+
}
|
|
301
|
+
emitQueueChanged(threadId) {
|
|
302
|
+
// Desktop mutates its local queue cache after the request promise resolves. Emitting
|
|
303
|
+
// synchronously lets its refresh replace that cache first; an edit then holds the old
|
|
304
|
+
// message id and fails with "queued follow-up no longer exists". Match app-server
|
|
305
|
+
// ordering: return the mutation response first, then notify this and other windows.
|
|
306
|
+
if (this.closed || this.queueNotifications.has(threadId)) return;
|
|
307
|
+
const timer = setTimeout(() => {
|
|
308
|
+
this.queueNotifications.delete(threadId);
|
|
309
|
+
this.emit({ method: 'thread/queue/changed', params: { threadId } });
|
|
310
|
+
}, 0);
|
|
311
|
+
timer.unref?.();
|
|
312
|
+
this.queueNotifications.set(threadId, timer);
|
|
313
|
+
}
|
|
314
|
+
thread(id) {
|
|
315
|
+
if (!id) return undefined;
|
|
316
|
+
return this.runtime.threads.some(t => t.id === id) ? this.runtime.getThread(id) : undefined;
|
|
317
|
+
}
|
|
318
|
+
owns(id) { return Boolean(this.thread(id)); }
|
|
319
|
+
turn(turn) {
|
|
320
|
+
// 官方 Turn 合约的时间字段:startedAt/completedAt(epoch 秒,Desktop 恢复线程时经
|
|
321
|
+
// kBt(x*1e3) 还原)与 durationMs(turn/completed 处理器直接采用)。缺失时 Desktop
|
|
322
|
+
// 无法合成 worked-for 计时项,完成回合只回退显示“已使用 Harness Mix 集成”。
|
|
323
|
+
const startedAt = turn.startedAt ?? turn.createdAt ?? null;
|
|
324
|
+
const completedAt = turn.completedAt ?? null;
|
|
325
|
+
return { id: turn.id, status: turnStatus(turn.status), error: turn.error ? { message: String(turn.error), codexErrorInfo: turn.codexErrorInfo ?? null, additionalDetails: null } : null,
|
|
326
|
+
startedAt: startedAt != null ? Math.floor(startedAt / 1000) : null,
|
|
327
|
+
completedAt: completedAt != null ? Math.floor(completedAt / 1000) : null,
|
|
328
|
+
durationMs: startedAt != null && completedAt != null ? Math.max(0, completedAt - startedAt) : null,
|
|
329
|
+
items: this.runtime.core.getItemsForTurn(turn.id).map(projectItem).filter(Boolean) };
|
|
330
|
+
}
|
|
331
|
+
projectThread(thread, includeTurns = true) {
|
|
332
|
+
// Projection shape mirrors the upstream harnessmix external-thread contract: every
|
|
333
|
+
// field the Desktop sidebar/composer reads must be present with the same defaults.
|
|
334
|
+
const updatedAt = Math.floor((thread.updatedAt || thread.createdAt) / 1000);
|
|
335
|
+
return { id: thread.id, preview: thread.messages?.find(m => m.role === 'user')?.text || thread.preview || thread.title,
|
|
336
|
+
ephemeral: thread.ephemeral === true, modelProvider: 'harnessmix', model: routeModel(externalId(thread.harnessId)), reasoningEffort: null,
|
|
337
|
+
section: thread.section ?? null, sectionEnteredAt: thread.sectionEnteredAt ?? null, projectId: thread.projectId ?? null,
|
|
338
|
+
createdAt: Math.floor(thread.createdAt / 1000),
|
|
339
|
+
updatedAt, recencyAt: updatedAt,
|
|
340
|
+
status: { type: thread.status === 'working' ? 'active' : 'idle', ...(thread.status === 'working' ? { activeFlags: [] } : {}) },
|
|
341
|
+
path: null, cwd: thread.cwd, cliVersion: 'harnessmix', source: thread.parentThreadId ? 'subAgentThreadSpawn' : 'vscode', threadSource: null,
|
|
342
|
+
name: thread.title || null, agentNickname: thread.parentThreadId ? this.runtime.adapters.get(thread.harnessId)?.manifest.name || thread.harnessId : null,
|
|
343
|
+
agentRole: thread.parentThreadId ? 'worker' : null, gitInfo: thread.gitInfo || null,
|
|
344
|
+
sessionId: thread.id, forkedFromId: thread.forkedFrom ?? null, parentThreadId: thread.parentThreadId ?? null,
|
|
345
|
+
// 跨 Harness 原地切换血缘:链上每条是某 Harness 曾用的原生会话引用(切回可 resume)
|
|
346
|
+
harnessChain: (thread.harnessChain ?? []).map(e => ({ harnessId: externalId(e.harnessId), at: e.at })),
|
|
347
|
+
pendingHarnessSwitch: thread.pendingHandoff ? {
|
|
348
|
+
checkpointId: thread.pendingHandoff.checkpointId,
|
|
349
|
+
fromHarnessId: externalId(thread.pendingHandoff.fromHarnessId),
|
|
350
|
+
toHarnessId: externalId(thread.pendingHandoff.toHarnessId || thread.harnessId),
|
|
351
|
+
phase: thread.pendingHandoff.phase || 'ready',
|
|
352
|
+
intent: thread.pendingHandoff.intent || 'continue',
|
|
353
|
+
note: thread.pendingHandoff.note ?? null,
|
|
354
|
+
} : null,
|
|
355
|
+
canAcceptDirectInput: true, historyMode: 'legacy', isPinned: false, extra: null,
|
|
356
|
+
isolation: thread.isolation ?? 'shared',
|
|
357
|
+
workspace: thread.workspace ? { mode: thread.workspace.mode, branch: thread.workspace.branch, root: thread.workspace.root } : null,
|
|
358
|
+
turns: includeTurns ? this.runtime.core.turns.turnsForThread(thread.id).map(t => this.turn(t)) : [] };
|
|
359
|
+
}
|
|
360
|
+
capabilities(id, catalog) {
|
|
361
|
+
const cap = this.runtime.getCapabilities(id);
|
|
362
|
+
return { configuration: { selectModel: Boolean(cap.model?.selection), selectThinkingOption: Boolean(cap.model?.thinkingLevel),
|
|
363
|
+
selectPermissionMode: Boolean(catalog.permissionModes?.length), permissionModeScope: 'live' },
|
|
364
|
+
history: { fork: Boolean(cap.session?.fork), forkAcrossCwd: false, rollbackLastTurn: Boolean(cap.session?.forkFromMessage) },
|
|
365
|
+
workspace: { git: true, worktree: true, finalDiff: true, nativeDiff: Boolean(cap.workspace?.nativeDiff), nativePatch: Boolean(cap.workspace?.nativePatch) } };
|
|
366
|
+
}
|
|
367
|
+
async inspect(id) {
|
|
368
|
+
const local = ALIASES[id] || id;
|
|
369
|
+
if (!this.runtime.adapters.has(local)) return { status: 'notInstalled', error: { code: 'UNSUPPORTED', message: `Harness Mix has no adapter for ${id}`, retryable: false } };
|
|
370
|
+
if (!this.runtime.status[local]?.available) return { status: 'unavailable', error: { code: 'UNAVAILABLE', message: this.runtime.status[local]?.detail || 'Harness unavailable', retryable: true } };
|
|
371
|
+
try {
|
|
372
|
+
let catalog = await this.runtime.describe(local);
|
|
373
|
+
if (catalog.models === null) {
|
|
374
|
+
const adapter = this.runtime.adapters.get(local);
|
|
375
|
+
const probe = await adapter.open({ thread: { cwd: os.tmpdir(), nativeSessionId: randomUUID(), options: {} }, emit: () => {}, diagnostic: () => {} });
|
|
376
|
+
try { catalog = await adapter.describeFor(probe); this.runtime.catalogs.set(local, catalog); }
|
|
377
|
+
finally { await adapter.close(probe); }
|
|
378
|
+
}
|
|
379
|
+
const thinkingOptions = (catalog.thinkingLevels || []).map(x => ({ id: x.id, label: x.label || x.id }));
|
|
380
|
+
const levelId = entry => (typeof entry === 'string' ? entry : entry?.id);
|
|
381
|
+
// 模型级 efforts(Claude/DSH/OpenCode 按模型声明)并入全局档位表(共享契约要求 supported ⊆ thinkingOptions)
|
|
382
|
+
for (const sourceModel of catalog.models || []) {
|
|
383
|
+
for (const entry of sourceModel.efforts || []) {
|
|
384
|
+
const id = levelId(entry);
|
|
385
|
+
if (id && !thinkingOptions.some(o => o.id === id)) {
|
|
386
|
+
thinkingOptions.push({ id, label: (typeof entry === 'object' && entry?.label) || id });
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// adapter 可在 thinkingLevels 某一项上声明 default: true(如 Antigravity 的 high),
|
|
391
|
+
// 作为该 Harness 的安全预选档;未声明则不预选,由原生会话自身默认档生效。
|
|
392
|
+
const defaultThinking = (catalog.thinkingLevels || []).find(x => x.default === true)?.id;
|
|
393
|
+
const thinkingIds = thinkingOptions.map(o => o.id);
|
|
394
|
+
const sourceModels = catalog.models || [];
|
|
395
|
+
const models = sourceModels.map(m => {
|
|
396
|
+
// 模型显式声明了 efforts(含空数组)以模型为准;未声明则适用全部全局档位
|
|
397
|
+
const supported = Array.isArray(m.efforts)
|
|
398
|
+
? m.efforts.map(levelId).filter(id => id && thinkingIds.includes(id))
|
|
399
|
+
: thinkingIds;
|
|
400
|
+
return { ref: modelRef(m), label: m.name || m.id,
|
|
401
|
+
...(supported.length ? { supportedThinkingOptionIds: supported } : {}) };
|
|
402
|
+
});
|
|
403
|
+
// The renderer draft flow requires a default model for its initial selection.
|
|
404
|
+
const defaultSource = sourceModels.find(m => m && m.isDefault === true) || sourceModels[0];
|
|
405
|
+
const result = { status: 'ready', catalog: { models, ...(defaultSource ? { defaultModel: modelRef(defaultSource) } : {}), thinkingOptions,
|
|
406
|
+
...(defaultThinking ? { defaultThinkingOptionId: defaultThinking } : {}) }, capabilities: this.capabilities(local, catalog) };
|
|
407
|
+
if (catalog.permissionModes?.length) result.permissionModes = { modes: catalog.permissionModes.map(m => ({ id: m.id, label: m.label || m.name || m.id, ...((m.description || m.hint) ? { description: m.description || m.hint } : {}), ...(m.dangerous ? { dangerous: true } : {}) })), defaultModeId: catalog.permissionModes.find(m => m.default)?.id || catalog.permissionModes[0].id };
|
|
408
|
+
return result;
|
|
409
|
+
} catch (error) {
|
|
410
|
+
// A harness binary that exited mid-probe is deterministic misconfiguration:
|
|
411
|
+
// retrying immediately would re-spawn it (possibly a full GUI) in a loop.
|
|
412
|
+
// The renderer only keeps polling agents whose error is retryable.
|
|
413
|
+
return { status: 'error', error: { code: 'INSPECTION_FAILED', message: error.message, retryable: !error.harnessExited } };
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
configuration(thread) {
|
|
417
|
+
const catalogEntry = this.runtime.catalogs?.get(thread.harnessId);
|
|
418
|
+
const models = catalogEntry?.models || thread.models || [];
|
|
419
|
+
const configured = thread.options?.model;
|
|
420
|
+
const actual = thread.model;
|
|
421
|
+
const exact = candidate => candidate && models.find(model => modelRef(model).id === modelRef(candidate).id);
|
|
422
|
+
// Native runtime names can be resolved aliases, and older Pi records omitted
|
|
423
|
+
// their provider. Prefer the explicitly selected catalog identity, then a
|
|
424
|
+
// unique native match; never guess between providers with the same model id.
|
|
425
|
+
const matches = actual ? models.filter(model => model.id === actual.id && (!actual.provider || model.provider === actual.provider)) : [];
|
|
426
|
+
const selected = exact(configured) || exact(actual) || (matches.length === 1 ? matches[0] : null) || actual || configured;
|
|
427
|
+
// 当前模型适用的思考档位:模型声明了 efforts 以模型为准,否则适用全局目录
|
|
428
|
+
const levelId = entry => (typeof entry === 'string' ? entry : entry?.id);
|
|
429
|
+
const globalLevels = (catalogEntry?.thinkingLevels || []).map(l => ({ id: l.id, label: l.label || l.id }));
|
|
430
|
+
const selectedEntry = selected ? models.find(m => m.id === selected.id && (!selected.provider || !m.provider || m.provider === selected.provider)) : null;
|
|
431
|
+
const thinkingOptions = Array.isArray(selectedEntry?.efforts)
|
|
432
|
+
? selectedEntry.efforts.map(entry => {
|
|
433
|
+
const id = levelId(entry);
|
|
434
|
+
return id ? { id, label: (typeof entry === 'object' && entry?.label) || id } : null;
|
|
435
|
+
}).filter(Boolean)
|
|
436
|
+
: globalLevels;
|
|
437
|
+
const thinking = thread.options?.thinking;
|
|
438
|
+
const selectedLabel = selectedEntry ? (selectedEntry.name || selectedEntry.id) : (typeof selected === 'object' && selected ? (selected.name || selected.id) : selected);
|
|
439
|
+
return { ...(selected ? { effectiveModel: modelRef(selected) } : {}),
|
|
440
|
+
// 渲染端目录未命中时兜底显示模型名(composer 优先用 resolvedModelLabel)
|
|
441
|
+
...(selectedLabel ? { resolvedModelLabel: String(selectedLabel) } : {}),
|
|
442
|
+
// 可选集合存在时,生效档位必须属于其中(共享契约校验);目录未加载时保留原样
|
|
443
|
+
...(thinking && (!thinkingOptions.length || thinkingOptions.some(o => o.id === thinking)) ? { effectiveThinkingOptionId: thinking } : {}),
|
|
444
|
+
...(thinkingOptions.length ? { availableThinkingOptions: thinkingOptions } : {}),
|
|
445
|
+
...(thread.options?.permissionMode ? { effectivePermissionModeId: thread.options.permissionMode } : {}) };
|
|
446
|
+
}
|
|
447
|
+
async resolveModel(harnessId, ref) {
|
|
448
|
+
if (!ref) return undefined;
|
|
449
|
+
const catalog = await this.runtime.describe(harnessId);
|
|
450
|
+
const model = catalog.models.find(m => modelRef(m).id === ref.id);
|
|
451
|
+
if (!model) throw new Error('Selected model is no longer in the native catalog');
|
|
452
|
+
return model;
|
|
453
|
+
}
|
|
454
|
+
async request(method, params = {}) {
|
|
455
|
+
if (method === 'harnessmix/integrations/catalog') return this.runtime.integrations.catalog();
|
|
456
|
+
if (method === 'harnessmix/integrations/list') return this.runtime.integrations.list(params);
|
|
457
|
+
if (method === 'harnessmix/integrations/mcp/save') return this.runtime.integrations.save(params);
|
|
458
|
+
if (method === 'harnessmix/integrations/mcp/remove') return this.runtime.integrations.remove(params);
|
|
459
|
+
if (method === 'harnessmix/integrations/skill/change') return this.runtime.integrations.skillChange(params);
|
|
460
|
+
if (method === 'harnessmix/harness/session-import/sources') return this.runtime.history.sources();
|
|
461
|
+
if (method === 'harnessmix/harness/session-import/list') return this.runtime.history.list(params);
|
|
462
|
+
if (method === 'harnessmix/harness/session-import/import') return this.runtime.history.import(params);
|
|
463
|
+
if (method === 'harnessmix/collaboration/agents') return [...this.runtime.adapters.values()].map(a => ({ id: externalId(a.manifest.id), name: a.manifest.name, available: !!this.runtime.status[a.manifest.id]?.available, lead: a.manifest.capabilities?.collaborationTools === true }));
|
|
464
|
+
if (method === 'harnessmix/collaboration/preferences') return this.runtime.collaboration.getPreferences();
|
|
465
|
+
if (method === 'harnessmix/collaboration/preferences/save') return this.runtime.collaboration.setPreferences(params);
|
|
466
|
+
// 桌宠市场:官方预载(Codex 安装包 asar 提取)与 ~/.codex/pets 安装管理
|
|
467
|
+
if (method === 'harnessmix/pets/catalog') return this.pets.catalog();
|
|
468
|
+
if (method === 'harnessmix/pets/community') return this.pets.community(params);
|
|
469
|
+
if (method === 'harnessmix/pets/preview') return this.pets.preview(params);
|
|
470
|
+
if (method === 'harnessmix/pets/install') return this.pets.install(params);
|
|
471
|
+
if (method === 'harnessmix/pets/uninstall') return this.pets.uninstall(params);
|
|
472
|
+
// Harness Mix 本地桌宠选择(仅驱动 Harness Mix 自有展示面,不触碰账号级 accessory_id)
|
|
473
|
+
if (method === 'harnessmix/pets/selection') return this.pets.selection();
|
|
474
|
+
if (method === 'harnessmix/pets/select') return this.pets.select(params);
|
|
475
|
+
const thread = this.thread(params.threadId);
|
|
476
|
+
// 失败回合分类查询:Renderer 据此在输入框下方渲染只真正帮得上忙的动作按钮
|
|
477
|
+
if (method === 'harnessmix/harness/turn-error') {
|
|
478
|
+
if (!thread) throw new Error('Unknown thread');
|
|
479
|
+
const kind = thread.errorKind ?? null;
|
|
480
|
+
const meta = HARNESS_AUTH_INFO[externalId(thread.harnessId)] || HARNESS_AUTH_INFO[thread.harnessId];
|
|
481
|
+
return {
|
|
482
|
+
threadId: thread.id,
|
|
483
|
+
error: thread.error ?? null,
|
|
484
|
+
errorKind: kind,
|
|
485
|
+
actions: kind ? errorActions(kind, { canLogin: Boolean(meta?.loginCommand) }) : [],
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
if (method === 'harness-mix/runtime/inspect') return {
|
|
489
|
+
owner: 'harness-mix',
|
|
490
|
+
runtime: 'src/main/host/runtime.js',
|
|
491
|
+
core: 'src/main/protocol-core/protocol-core.js',
|
|
492
|
+
threads: this.runtime.threads.length,
|
|
493
|
+
codex: {
|
|
494
|
+
mode: 'official-passthrough',
|
|
495
|
+
// `codex` remains owned by the stock app-server. Only the explicit
|
|
496
|
+
// `codex-harness` route creates a HostRuntime-managed Codex worker.
|
|
497
|
+
managedRoute: 'codex-harness',
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
if (method === 'harness-mix/runtime/version') {
|
|
501
|
+
let version = '0.0.0';
|
|
502
|
+
try { version = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf8')).version || version; } catch {}
|
|
503
|
+
return { version };
|
|
504
|
+
}
|
|
505
|
+
// Updates
|
|
506
|
+
if (method === 'harnessmix/update/check') {
|
|
507
|
+
const dataDir = nativeEnvironment().HARNESSMIX_DATA_DIR;
|
|
508
|
+
let currentVersion = '0.1.2';
|
|
509
|
+
try {
|
|
510
|
+
currentVersion = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf8')).version || '0.1.2';
|
|
511
|
+
} catch { /* fallback */ }
|
|
512
|
+
const channel = detectChannel(REPO_ROOT);
|
|
513
|
+
let latestVersion = currentVersion;
|
|
514
|
+
let updateAvailable = false;
|
|
515
|
+
let checkError = null;
|
|
516
|
+
|
|
517
|
+
if (channel === 'git') {
|
|
518
|
+
try {
|
|
519
|
+
// Async runner: a sync git fetch here would freeze the whole host
|
|
520
|
+
// (heartbeats, approvals, every thread) for the fetch timeout.
|
|
521
|
+
const remote = await remoteState(REPO_ROOT, makeGit(REPO_ROOT, asyncRun()));
|
|
522
|
+
if (remote.state === 'available') {
|
|
523
|
+
updateAvailable = true;
|
|
524
|
+
latestVersion = `${currentVersion}+git.${remote.remote.slice(0, 7)}`;
|
|
525
|
+
} else {
|
|
526
|
+
latestVersion = currentVersion;
|
|
527
|
+
}
|
|
528
|
+
} catch (err) {
|
|
529
|
+
checkError = err.message;
|
|
530
|
+
}
|
|
531
|
+
} else {
|
|
532
|
+
try {
|
|
533
|
+
const fetched = await fetchLatestVersion({
|
|
534
|
+
registry: await resolveRegistry({ run: asyncRun() }),
|
|
535
|
+
dataDir,
|
|
536
|
+
timeoutMs: 6000,
|
|
537
|
+
});
|
|
538
|
+
if (fetched) {
|
|
539
|
+
latestVersion = fetched;
|
|
540
|
+
updateAvailable = compareVersions(latestVersion, currentVersion) > 0;
|
|
541
|
+
}
|
|
542
|
+
} catch (err) {
|
|
543
|
+
checkError = err.message;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return {
|
|
548
|
+
currentVersion,
|
|
549
|
+
installation: channel === 'npm' ? 'npm' : 'windows-installer',
|
|
550
|
+
latestVersion,
|
|
551
|
+
updateAvailable,
|
|
552
|
+
installationAvailable: true,
|
|
553
|
+
releaseNotes: updateAvailable ? `发现新版本 ${latestVersion},支持自动快进更新与安全回滚。` : null,
|
|
554
|
+
releaseNotesUrl: `https://github.com/emo-xiaoyu/harness-mix/releases/tag/v${currentVersion}`,
|
|
555
|
+
status: null,
|
|
556
|
+
error: checkError ? `更新检查失败: ${checkError.slice(0, 450)}` : null,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
if (method === 'harnessmix/update/start') {
|
|
560
|
+
const dataDir = nativeEnvironment().HARNESSMIX_DATA_DIR;
|
|
561
|
+
const channel = detectChannel(REPO_ROOT);
|
|
562
|
+
let currentVersion = '0.1.2';
|
|
563
|
+
try { currentVersion = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf8')).version || '0.1.2'; } catch {}
|
|
564
|
+
const installation = channel === 'npm' ? 'npm' : 'windows-installer';
|
|
565
|
+
// The heavy flow must run in a detached helper, never in this host:
|
|
566
|
+
// 1) process.stdout here is the JSONL protocol channel to the Desktop —
|
|
567
|
+
// updater/npm/cargo output would corrupt it;
|
|
568
|
+
// 2) minutes of synchronous git/npm/cargo work would freeze the event
|
|
569
|
+
// loop (heartbeats, approvals, every thread);
|
|
570
|
+
// 3) the host lives inside the shim's kill-on-close job — it cannot
|
|
571
|
+
// stop the Desktop without killing itself mid-update.
|
|
572
|
+
// The helper defers what the running Desktop locks and stops it last;
|
|
573
|
+
// the Desktop keeps polling harnessmix/update/status from the state file.
|
|
574
|
+
try {
|
|
575
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
576
|
+
const logFile = path.join(dataDir, 'update.log');
|
|
577
|
+
const fd = fs.openSync(logFile, 'a');
|
|
578
|
+
try {
|
|
579
|
+
const helper = spawn(process.execPath, [path.join(REPO_ROOT, 'scripts', 'native-update.cjs')], {
|
|
580
|
+
stdio: ['ignore', fd, fd],
|
|
581
|
+
detached: true,
|
|
582
|
+
windowsHide: true,
|
|
583
|
+
env: process.env,
|
|
584
|
+
});
|
|
585
|
+
helper.unref();
|
|
586
|
+
} finally {
|
|
587
|
+
fs.closeSync(fd);
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
status: {
|
|
591
|
+
version: currentVersion,
|
|
592
|
+
installation,
|
|
593
|
+
phase: 'installing',
|
|
594
|
+
updatedAt: Date.now(),
|
|
595
|
+
error: null,
|
|
596
|
+
},
|
|
597
|
+
};
|
|
598
|
+
} catch (err) {
|
|
599
|
+
return {
|
|
600
|
+
status: {
|
|
601
|
+
version: currentVersion,
|
|
602
|
+
installation,
|
|
603
|
+
phase: 'failed',
|
|
604
|
+
updatedAt: Date.now(),
|
|
605
|
+
error: `无法启动更新助手:${err.message}`.slice(0, 450),
|
|
606
|
+
},
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (method === 'harnessmix/update/status') {
|
|
611
|
+
const dataDir = nativeEnvironment().HARNESSMIX_DATA_DIR;
|
|
612
|
+
const state = readState(dataDir);
|
|
613
|
+
const channel = detectChannel(REPO_ROOT);
|
|
614
|
+
let currentVersion = '0.1.2';
|
|
615
|
+
try { currentVersion = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf8')).version || '0.1.2'; } catch {}
|
|
616
|
+
if (state.phase && state.phase !== 'idle') {
|
|
617
|
+
return {
|
|
618
|
+
status: {
|
|
619
|
+
version: state.pendingVersion || state.appliedVersion || currentVersion,
|
|
620
|
+
installation: channel === 'npm' ? 'npm' : 'windows-installer',
|
|
621
|
+
phase: state.phase === 'applying' ? 'installing' : 'succeeded',
|
|
622
|
+
updatedAt: state.appliedAt || Date.now(),
|
|
623
|
+
error: null,
|
|
624
|
+
},
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
return { status: null };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Each extra Account is an isolated native CODEX_HOME. Harness Mix stores
|
|
631
|
+
// only the profile label/path; auth.json and token refresh remain owned by Codex.
|
|
632
|
+
if (method === 'harnessmix/account/list') return this.codexAccounts.list(false);
|
|
633
|
+
if (method === 'harnessmix/account/refresh') return this.codexAccounts.list(true);
|
|
634
|
+
if (method === 'harnessmix/account/create') return this.codexAccounts.create(params.label);
|
|
635
|
+
if (method === 'harnessmix/account/delete') return this.codexAccounts.delete(params.accountId);
|
|
636
|
+
if (method === 'harnessmix/account/activate') return this.codexAccounts.activate(params.accountId);
|
|
637
|
+
if (method === 'harnessmix/account/usage/inspect') return this.codexAccounts.usage(params.accountId);
|
|
638
|
+
if (method === 'harnessmix/account/login/start') return this.codexAccounts.startLogin(params.accountId);
|
|
639
|
+
if (method === 'harnessmix/account/login/cancel') return this.codexAccounts.cancelLogin(params.loginId);
|
|
640
|
+
if (method === 'harnessmix/account/logout') {
|
|
641
|
+
if (!this.requestOfficial) throw new Error('Official Codex logout is unavailable');
|
|
642
|
+
await this.requestOfficial('account/logout', undefined);
|
|
643
|
+
return this.codexAccounts.signedOutOfficial();
|
|
644
|
+
}
|
|
645
|
+
if (method === 'harnessmix/account/rate-limit-reset/consume') return this.codexAccounts.consumeReset(params.accountId, params.idempotencyKey);
|
|
646
|
+
if (method === 'harnessmix/harness/account/login') {
|
|
647
|
+
const extId = params.harnessId;
|
|
648
|
+
const meta = HARNESS_AUTH_INFO[extId] || HARNESS_AUTH_INFO[ALIASES[extId]];
|
|
649
|
+
const cmd = meta?.loginCommand;
|
|
650
|
+
if (!cmd) throw new Error(`Harness ${extId} 暂不支持命令行直接登录,请参考凭据配置指引进行配置`);
|
|
651
|
+
const title = `登录 ${meta.name || extId}`;
|
|
652
|
+
const execCmd = `cmd.exe /c start "${title}" cmd.exe /k "echo 正在为 ${meta.name} 启动登录认证... && echo 执行命令: ${cmd} && echo. && ${cmd} && echo. && echo [Harness Mix] 登录操作已完成,请关闭此窗口并返回 Harness Mix 刷新状态"`;
|
|
653
|
+
exec(execCmd, { windowsHide: false });
|
|
654
|
+
return { success: true, command: cmd };
|
|
655
|
+
}
|
|
656
|
+
if (method === 'harnessmix/harness/accounts/list') {
|
|
657
|
+
const accounts = [];
|
|
658
|
+
for (const adapter of this.runtime.adapters.values()) {
|
|
659
|
+
const id = adapter.manifest.id;
|
|
660
|
+
const extId = externalId(id);
|
|
661
|
+
const meta = HARNESS_AUTH_INFO[extId] || HARNESS_AUTH_INFO[id] || { name: adapter.manifest.name };
|
|
662
|
+
const isAvailable = !!this.runtime.status[id]?.available;
|
|
663
|
+
|
|
664
|
+
let email = undefined;
|
|
665
|
+
let label = meta.label;
|
|
666
|
+
let plan = meta.plan;
|
|
667
|
+
let credits = undefined;
|
|
668
|
+
let status = isAvailable ? 'ready' : 'not_installed';
|
|
669
|
+
|
|
670
|
+
if (typeof adapter.inspectAccount === 'function') {
|
|
671
|
+
try {
|
|
672
|
+
const raw = await Promise.race([
|
|
673
|
+
Promise.resolve().then(() => adapter.inspectAccount()),
|
|
674
|
+
new Promise((resolve) => setTimeout(() => resolve(null), 5_000)),
|
|
675
|
+
]);
|
|
676
|
+
if (raw && typeof raw === 'object') {
|
|
677
|
+
if (raw.email && typeof raw.email === 'string') email = raw.email.trim();
|
|
678
|
+
if (raw.label && typeof raw.label === 'string') label = raw.label.trim();
|
|
679
|
+
if (raw.plan && typeof raw.plan === 'string') plan = raw.plan.trim();
|
|
680
|
+
if (raw.credits) credits = projectAccountCredits(raw.credits) || undefined;
|
|
681
|
+
status = 'ready';
|
|
682
|
+
}
|
|
683
|
+
} catch {
|
|
684
|
+
// keep default
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (!email) {
|
|
689
|
+
if (id === 'codex') {
|
|
690
|
+
status = 'ready';
|
|
691
|
+
email = 'Codex Desktop 会话已就绪';
|
|
692
|
+
} else if (id === 'dsh') {
|
|
693
|
+
if (process.env.DEEPSEEK_API_KEY) {
|
|
694
|
+
status = 'ready';
|
|
695
|
+
email = 'API Key (已设置)';
|
|
696
|
+
} else {
|
|
697
|
+
status = isAvailable ? 'unconfigured' : 'not_installed';
|
|
698
|
+
}
|
|
699
|
+
} else if (id === 'grok') {
|
|
700
|
+
if (process.env.XAI_API_KEY) {
|
|
701
|
+
status = 'ready';
|
|
702
|
+
email = 'xAI API Key (已设置)';
|
|
703
|
+
} else {
|
|
704
|
+
status = isAvailable ? 'unconfigured' : 'not_installed';
|
|
705
|
+
}
|
|
706
|
+
} else if (id === 'openclaw') {
|
|
707
|
+
if (process.env.OPENCLAW_TOKEN) {
|
|
708
|
+
status = 'ready';
|
|
709
|
+
email = 'Gateway Token (已配置)';
|
|
710
|
+
} else {
|
|
711
|
+
status = isAvailable ? 'unconfigured' : 'not_installed';
|
|
712
|
+
}
|
|
713
|
+
} else if (!isAvailable) {
|
|
714
|
+
status = 'not_installed';
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
accounts.push({
|
|
719
|
+
harnessId: extId,
|
|
720
|
+
harnessName: meta.name || adapter.manifest.name,
|
|
721
|
+
status,
|
|
722
|
+
...(email ? { email } : {}),
|
|
723
|
+
...(label ? { label } : {}),
|
|
724
|
+
...(plan ? { plan } : {}),
|
|
725
|
+
...(meta.configHint ? { configHint: meta.configHint } : {}),
|
|
726
|
+
...(meta.loginCommand ? { loginCommand: meta.loginCommand } : {}),
|
|
727
|
+
...(credits ? { credits } : {}),
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
return { accounts };
|
|
731
|
+
}
|
|
732
|
+
if (method === 'harnessmix/harness/plugins/list') return { plugins: this.runtime.snapshot().adapters.map(a => ({ id: externalId(a.id), name: a.id === 'codex' ? 'Codex(协作)' : a.name, version: '0.1.0', icon: `data:image/svg+xml;base64,${Buffer.from(getHarnessSvg(a.id)).toString('base64')}` })) };
|
|
733
|
+
if (method === 'harnessmix/storage/inspect') return this.runtime.inspectStorage();
|
|
734
|
+
if (method === 'harnessmix/storage/optimize') return this.runtime.optimizeStorage();
|
|
735
|
+
if (method === 'harnessmix/harness/inspect') return this.inspect(params.harnessId);
|
|
736
|
+
if (method === 'harnessmix/harness/install') {
|
|
737
|
+
const local = ALIASES[params.harnessId] || params.harnessId;
|
|
738
|
+
const entry = HARNESS_INSTALL_COMMANDS[params.harnessId] || HARNESS_INSTALL_COMMANDS[local];
|
|
739
|
+
if (!entry) throw new Error(`No installation command available for harness: ${params.harnessId}`);
|
|
740
|
+
const command = (process.platform === 'win32' ? entry.win32 : entry.default) || entry.default;
|
|
741
|
+
|
|
742
|
+
if (params.terminal) {
|
|
743
|
+
const title = `安装 ${params.harnessId}`;
|
|
744
|
+
const cmd = `cmd.exe /c start "${title}" cmd.exe /k "echo 正在安装 ${params.harnessId} (${command})... && ${command} && echo. && echo [Harness Mix] 安装执行完毕,请关闭此窗口并返回 Harness Mix 点击【刷新检测】"`;
|
|
745
|
+
exec(cmd, { windowsHide: false });
|
|
746
|
+
return { success: true, command, stdout: '已在独立终端窗口中启动安装' };
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return new Promise((resolve) => {
|
|
750
|
+
exec(command, { timeout: 600000, shell: true, maxBuffer: 10 * 1024 * 1024 }, async (err, stdout, stderr) => {
|
|
751
|
+
const adapter = this.runtime.adapters.get(local);
|
|
752
|
+
if (adapter) {
|
|
753
|
+
try {
|
|
754
|
+
const inspection = await adapter.inspect();
|
|
755
|
+
this.runtime.status[local] = inspection;
|
|
756
|
+
if (inspection.available) this.runtime.catalogs.delete(local);
|
|
757
|
+
} catch (e) {
|
|
758
|
+
this.runtime.status[local] = { available: false, detail: e.message };
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
if (err) {
|
|
762
|
+
resolve({ success: false, command, error: err.message, stdout: stdout?.toString() || '', stderr: stderr?.toString() || '' });
|
|
763
|
+
} else {
|
|
764
|
+
resolve({ success: true, command, stdout: stdout?.toString() || '', stderr: stderr?.toString() || '' });
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
if (method === 'harnessmix/harness/commands/inspect' || method === 'harnessmix/thread/commands/inspect') {
|
|
770
|
+
const commands = await this.runtime.listCommands({ threadId: params.threadId, harnessId: ALIASES[params.harnessId] || params.harnessId });
|
|
771
|
+
return { commands: commands.map(c => ({ id: c.id, invocation: '/' + c.id, label: c.label || c.id, ...(c.description ? { description: c.description.slice(0, 512) } : {}), argumentMode: c.action === 'insert' ? 'text' : 'none' })) };
|
|
772
|
+
}
|
|
773
|
+
if (method === 'harnessmix/thread/ownership/list') return { threads: params.threadIds.map(id => ({ threadId: id, owner: this.owns(id) ? 'external' : 'codex', ...(this.owns(id) ? { harnessId: externalId(this.thread(id).harnessId) } : {}) })) };
|
|
774
|
+
if (method === 'harnessmix/thread/team/inspect') return this.runtime.collaboration.inspectTeam(params.threadId, params.teamId);
|
|
775
|
+
if (method === 'harnessmix/thread/inspect') {
|
|
776
|
+
if (!thread) return { owner: 'codex', locked: true };
|
|
777
|
+
const catalog = await this.runtime.describe(thread.harnessId);
|
|
778
|
+
const usage = typeof this.runtime.core?.getThread === 'function' ? this.runtime.core.getThread(thread.id)?.usage : undefined;
|
|
779
|
+
const adapter = this.runtime.adapters && typeof this.runtime.adapters.get === 'function' ? this.runtime.adapters.get(thread.harnessId) : null;
|
|
780
|
+
const rawCredits = adapter && typeof adapter.credits === 'function' ? adapter.credits() : null;
|
|
781
|
+
const credits = projectAccountCredits(rawCredits);
|
|
782
|
+
// projectUsage() returns null for an empty source (harnesses that never
|
|
783
|
+
// report usage, e.g. qodercli). The external inspection schema accepts an
|
|
784
|
+
// absent usage but not an explicit null: emitting "usage": null made the
|
|
785
|
+
// renderer's strict parse fail and wedge the whole ownership restore.
|
|
786
|
+
const projectedUsage = usage ? projectUsage(usage) : null;
|
|
787
|
+
return { owner: 'external', harnessId: externalId(thread.harnessId), transportModelId: routeModel(externalId(thread.harnessId)), locked: true,
|
|
788
|
+
...this.configuration(thread), history: this.capabilities(thread.harnessId, catalog).history,
|
|
789
|
+
workspace: await this.runtime.inspectThreadWorkspace(thread.id),
|
|
790
|
+
...(projectedUsage ? { usage: projectedUsage } : {}),
|
|
791
|
+
...(credits ? { accountCredits: credits } : {}) };
|
|
792
|
+
}
|
|
793
|
+
if (method === 'thread/start') {
|
|
794
|
+
const route = decodeRoute(params.model);
|
|
795
|
+
const accountContext = typeof params.__harnessmixAccountId === 'string'
|
|
796
|
+
? this.codexAccounts.executionContext(params.__harnessmixAccountId) : null;
|
|
797
|
+
if (!route && !accountContext) return undefined;
|
|
798
|
+
const effectiveRoute = route || { harnessId: 'codex-harness', ...(typeof params.model === 'string' ? { model: { id: params.model } } : {}) };
|
|
799
|
+
const id = ALIASES[effectiveRoute.harnessId] || effectiveRoute.harnessId;
|
|
800
|
+
if (!this.runtime.adapters.has(id)) throw new Error(`Unsupported Harness: ${id}`);
|
|
801
|
+
if (params.ephemeral && params.threadSource) throw new Error('Ephemeral background thread is not supported');
|
|
802
|
+
const isWorktree = params.worktree === true || params.options?.worktree === true;
|
|
803
|
+
const selectedModel = accountContext && typeof params.model === 'string'
|
|
804
|
+
? (await this.runtime.describe(id)).models.find(model => model.id === params.model)
|
|
805
|
+
: await this.resolveModel(id, effectiveRoute.model);
|
|
806
|
+
const created = await this.runtime.createThread({ harnessId: id, cwd: params.cwd, ephemeral: params.ephemeral === true,
|
|
807
|
+
worktree: isWorktree,
|
|
808
|
+
options: {
|
|
809
|
+
model: selectedModel, thinking: effectiveRoute.thinkingOptionId, permissionMode: effectiveRoute.permissionModeId,
|
|
810
|
+
...(accountContext || {}),
|
|
811
|
+
...(isWorktree ? { worktree: true } : {}),
|
|
812
|
+
...(pickTurnPermissions(params) ? { turnPermissions: pickTurnPermissions(params) } : {}),
|
|
813
|
+
} });
|
|
814
|
+
if (created.error) throw new Error(created.error);
|
|
815
|
+
const result = { thread: this.projectThread(created), model: params.model, modelProvider: 'harness-mix', cwd: created.cwd,
|
|
816
|
+
approvalPolicy: params.approvalPolicy || 'on-request', sandbox: params.sandbox || { type: 'workspaceWrite', writableRoots: [created.cwd], networkAccess: false, excludeTmpdirEnvVar: false, excludeSlashTmp: false }, reasoningEffort: null };
|
|
817
|
+
// thread/started 已由 runtime 的 thread-created 监听统一发出(含协作子任务路径)
|
|
818
|
+
return result;
|
|
819
|
+
}
|
|
820
|
+
if (method === 'harnessmix/thread/workspace/review') return this.runtime.reviewThreadWorkspace(params.threadId);
|
|
821
|
+
if (method === 'harnessmix/thread/workspace/apply') return this.runtime.applyThreadWorkspace(params.threadId, params.digest);
|
|
822
|
+
if (method === 'harnessmix/thread/workspace/discard') return this.runtime.discardThreadWorkspace(params.threadId);
|
|
823
|
+
if (method === 'harnessmix/thread/workspace/push') return this.runtime.pushThreadWorkspace(params.threadId, params);
|
|
824
|
+
if (method === 'harnessmix/thread/verification/get') return this.runtime.verificationState(params.threadId);
|
|
825
|
+
if (method === 'harnessmix/thread/verification/configure') return this.runtime.configureVerification(params.threadId, params.policy);
|
|
826
|
+
if (method === 'harnessmix/thread/verification/run') return this.runtime.runVerification(params.threadId);
|
|
827
|
+
if (!thread) {
|
|
828
|
+
if (method.startsWith('harnessmix/')) throw new Error(`Harness Mix does not implement ${method}`);
|
|
829
|
+
return undefined;
|
|
830
|
+
}
|
|
831
|
+
if (method === 'thread/read') return { thread: this.projectThread(thread, params.includeTurns !== false) };
|
|
832
|
+
if (method === 'thread/queue/list') {
|
|
833
|
+
const queue = this.getQueue(thread.id);
|
|
834
|
+
return {
|
|
835
|
+
data: queue.map(item => ({
|
|
836
|
+
id: item.id,
|
|
837
|
+
input: item.input,
|
|
838
|
+
clientUserMessageId: item.clientUserMessageId,
|
|
839
|
+
createdAt: item.createdAt,
|
|
840
|
+
})),
|
|
841
|
+
nextCursor: null,
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
if (method === 'thread/queue/add') {
|
|
845
|
+
const input = Array.isArray(params.input) ? params.input : [];
|
|
846
|
+
if (!input.length) throw new Error('Queue submission requires non-empty input');
|
|
847
|
+
const queue = this.getQueue(thread.id);
|
|
848
|
+
const submission = {
|
|
849
|
+
id: randomUUID(),
|
|
850
|
+
input,
|
|
851
|
+
clientUserMessageId: typeof params.clientUserMessageId === 'string' && params.clientUserMessageId ? params.clientUserMessageId : null,
|
|
852
|
+
createdAt: Date.now(),
|
|
853
|
+
};
|
|
854
|
+
queue.push(submission);
|
|
855
|
+
this.emitQueueChanged(thread.id);
|
|
856
|
+
return { queuedSubmission: submission };
|
|
857
|
+
}
|
|
858
|
+
if (method === 'thread/queue/delete') {
|
|
859
|
+
const queue = this.getQueue(thread.id);
|
|
860
|
+
const idx = queue.findIndex(item => item.id === params.queuedSubmissionId);
|
|
861
|
+
if (idx !== -1) {
|
|
862
|
+
queue.splice(idx, 1);
|
|
863
|
+
this.emitQueueChanged(thread.id);
|
|
864
|
+
return { deleted: true };
|
|
865
|
+
}
|
|
866
|
+
return { deleted: false };
|
|
867
|
+
}
|
|
868
|
+
if (method === 'thread/queue/update') {
|
|
869
|
+
const queue = this.getQueue(thread.id);
|
|
870
|
+
const item = queue.find(it => it.id === params.queuedSubmissionId);
|
|
871
|
+
if (!item) throw new Error('Queued submission not found');
|
|
872
|
+
if (Array.isArray(params.input)) item.input = params.input;
|
|
873
|
+
this.emitQueueChanged(thread.id);
|
|
874
|
+
return { queuedSubmission: item };
|
|
875
|
+
}
|
|
876
|
+
if (method === 'thread/queue/reorder') {
|
|
877
|
+
const queue = this.getQueue(thread.id);
|
|
878
|
+
const ids = Array.isArray(params.queuedSubmissionIds) ? params.queuedSubmissionIds : [];
|
|
879
|
+
const map = new Map(queue.map(item => [item.id, item]));
|
|
880
|
+
const reordered = ids.map(id => map.get(id)).filter(Boolean);
|
|
881
|
+
for (const item of queue) {
|
|
882
|
+
if (!ids.includes(item.id)) reordered.push(item);
|
|
883
|
+
}
|
|
884
|
+
this.queues.set(thread.id, reordered);
|
|
885
|
+
this.emitQueueChanged(thread.id);
|
|
886
|
+
return {};
|
|
887
|
+
}
|
|
888
|
+
if (method === 'thread/queue/start') {
|
|
889
|
+
const queue = this.getQueue(thread.id);
|
|
890
|
+
const idx = params.queuedSubmissionId
|
|
891
|
+
? queue.findIndex(item => item.id === params.queuedSubmissionId)
|
|
892
|
+
: 0;
|
|
893
|
+
if (idx === -1 || !queue[idx]) throw new Error('Queued submission not found');
|
|
894
|
+
return { turn: this.turn(await this.startQueuedSubmission(thread, queue[idx])) };
|
|
895
|
+
}
|
|
896
|
+
if (method === 'thread/metadata/update') return { thread: this.projectThread(await this.runtime.updateThreadMetadata(thread.id, params.gitInfo)) };
|
|
897
|
+
// Desktop 权限菜单(请求批准/帮我批准/完全访问)推送的线程级设置:原样存储并持久化,
|
|
898
|
+
// 后续回合经 codex 适配器转发到原生 app-server,让显示选择与实际生效一致
|
|
899
|
+
if (method === 'thread/settings/update') {
|
|
900
|
+
const picked = pickTurnPermissions(params);
|
|
901
|
+
if (picked) await this.runtime.setOptions(thread.id, { turnPermissions: { ...(thread.options?.turnPermissions ?? {}), ...picked } });
|
|
902
|
+
return {};
|
|
903
|
+
}
|
|
904
|
+
if (method === 'thread/section/move') {
|
|
905
|
+
if (params.sectionId !== null && (typeof params.sectionId !== 'string' || !params.sectionId)) throw new Error('Invalid sectionId');
|
|
906
|
+
let section = null;
|
|
907
|
+
if (params.sectionId !== null) {
|
|
908
|
+
if (!this.requestOfficial) throw new Error('Official section catalog unavailable');
|
|
909
|
+
let cursor = null;
|
|
910
|
+
do {
|
|
911
|
+
const page = await this.requestOfficial('threadSection/list', { cursor, limit: 100 });
|
|
912
|
+
section = page.data.find(entry => entry.id === params.sectionId) || null;
|
|
913
|
+
cursor = page.nextCursor;
|
|
914
|
+
} while (!section && cursor);
|
|
915
|
+
if (!section) throw new Error('Unknown thread section');
|
|
916
|
+
}
|
|
917
|
+
await this.runtime.setThreadSection(thread.id, section, params.beforeThreadId);
|
|
918
|
+
return {};
|
|
919
|
+
}
|
|
920
|
+
if (method === 'thread/resume') {
|
|
921
|
+
// 回显线程实际生效的权限(Desktop 据此渲染 composer 权限指示),而不是硬编码默认值
|
|
922
|
+
const perms = thread.options?.turnPermissions ?? null;
|
|
923
|
+
return { thread: this.projectThread(thread), model: routeModel(externalId(thread.harnessId)), modelProvider: 'harness-mix', cwd: thread.cwd,
|
|
924
|
+
approvalPolicy: perms?.approvalPolicy ?? 'on-request', approvalsReviewer: perms?.approvalsReviewer ?? null,
|
|
925
|
+
sandbox: perms?.sandboxPolicy ?? { type: 'workspaceWrite', writableRoots: [thread.cwd], networkAccess: false, excludeTmpdirEnvVar: false, excludeSlashTmp: false }, reasoningEffort: null };
|
|
926
|
+
}
|
|
927
|
+
if (method === 'turn/start') {
|
|
928
|
+
const { text, attachments } = await prepareInput(params.input, thread.cwd);
|
|
929
|
+
// Desktop fallback 路径在每个回合都携带完整权限参数;缺失时由适配器回退到线程级设置
|
|
930
|
+
return { turn: this.turn(await this.startNativeTurn(thread, text, attachments, undefined, pickTurnPermissions(params))) };
|
|
931
|
+
}
|
|
932
|
+
// External steering: cancel the active Turn, wait for it to fully settle, then start
|
|
933
|
+
// the new input as a real new Turn. Never guess a stale target, never auto-start on
|
|
934
|
+
// failure, and never let a concurrent start race the replacement.
|
|
935
|
+
if (method === 'turn/steer') {
|
|
936
|
+
const input = Array.isArray(params.input) ? params.input : [];
|
|
937
|
+
// Reject before touching the active Turn; nothing is silently dropped mid-flight.
|
|
938
|
+
if (!input.length) {
|
|
939
|
+
throw new Error('Native steering currently accepts non-empty text input only');
|
|
940
|
+
}
|
|
941
|
+
const { text, attachments } = await prepareInput(input, thread.cwd);
|
|
942
|
+
if (!text.trim() && !attachments.length) throw new Error('Native steering requires non-empty text or images');
|
|
943
|
+
if (attachments.length && !this.runtime.getCapabilities(thread.harnessId).conversation.attachments) throw new Error('当前 Harness 不支持图片附件');
|
|
944
|
+
const expectedTurnId = params.expectedTurnId;
|
|
945
|
+
if (typeof expectedTurnId !== 'string' || !expectedTurnId) throw new Error('Native steering requires the active Turn identity');
|
|
946
|
+
return this.steerThread(thread, {
|
|
947
|
+
expectedTurnId,
|
|
948
|
+
text,
|
|
949
|
+
attachments,
|
|
950
|
+
messageKey: typeof params.clientUserMessageId === 'string' && params.clientUserMessageId
|
|
951
|
+
? `${thread.id}\0${params.clientUserMessageId}` : null,
|
|
952
|
+
fingerprint: JSON.stringify({ expectedTurnId, input }),
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
if (method === 'turn/interrupt') { await this.runtime.cancel(thread.id); return {}; }
|
|
956
|
+
// 跨 Harness 任务协作:委派新子任务 / 跟进既有子任务,等待链由父线程协作 Turn 承载
|
|
957
|
+
if (method === 'harnessmix/thread/delegate' || method === 'harnessmix/thread/message') {
|
|
958
|
+
const task = params.task ?? params.text;
|
|
959
|
+
if (typeof task !== 'string' || !task.trim()) throw new Error('Delegation requires a non-empty task');
|
|
960
|
+
const { child, turn } = await this.runtime.delegateTask({
|
|
961
|
+
fromThreadId: thread.id,
|
|
962
|
+
harnessId: method === 'harnessmix/thread/delegate' ? (ALIASES[params.harnessId] || params.harnessId) : undefined,
|
|
963
|
+
childThreadId: method === 'harnessmix/thread/message' ? params.childThreadId : undefined,
|
|
964
|
+
task,
|
|
965
|
+
});
|
|
966
|
+
return { turn: this.turn(turn), childThreadId: child.id };
|
|
967
|
+
}
|
|
968
|
+
if (method === 'thread/rollback') return { thread: this.projectThread(await this.runtime.rollbackThread(thread.id, params.numTurns)) };
|
|
969
|
+
if (method === 'thread/name/set') { await this.runtime.renameThread(thread.id, params.name); return {}; }
|
|
970
|
+
if (method === 'thread/archive' || method === 'thread/unarchive') {
|
|
971
|
+
const isArchive = method === 'thread/archive';
|
|
972
|
+
await this.runtime.setThreadArchived(thread.id, isArchive);
|
|
973
|
+
if (isArchive) {
|
|
974
|
+
this.emit({ method: 'thread/archived', params: { threadId: thread.id } });
|
|
975
|
+
return {};
|
|
976
|
+
}
|
|
977
|
+
const projected = this.projectThread(thread);
|
|
978
|
+
this.emit({ method: 'thread/unarchived', params: { thread: projected } });
|
|
979
|
+
return { thread: projected };
|
|
980
|
+
}
|
|
981
|
+
if (method === 'thread/loaded/list') return { data: [...this.runtime.sessions.keys()] };
|
|
982
|
+
if (method === 'harnessmix/thread/usage/inspect') {
|
|
983
|
+
const usage = params.refresh === 'exact' ? await this.runtime.refreshUsage(thread.id) : this.runtime.core.getThread(thread.id)?.usage;
|
|
984
|
+
const adapter = this.runtime.adapters.get(thread.harnessId);
|
|
985
|
+
let rawCredits = adapter && typeof adapter.credits === 'function' ? adapter.credits() : null;
|
|
986
|
+
if (!rawCredits && adapter && typeof adapter.refreshCredits === 'function') {
|
|
987
|
+
try { rawCredits = await adapter.refreshCredits(); } catch {}
|
|
988
|
+
} else if (adapter && params.refresh === 'exact' && typeof adapter.refreshCredits === 'function') {
|
|
989
|
+
try { await adapter.refreshCredits(); } catch {}
|
|
990
|
+
rawCredits = adapter && typeof adapter.credits === 'function' ? adapter.credits() : null;
|
|
991
|
+
}
|
|
992
|
+
const credits = projectAccountCredits(rawCredits);
|
|
993
|
+
return {
|
|
994
|
+
threadId: thread.id,
|
|
995
|
+
usage: projectUsage(usage),
|
|
996
|
+
...(credits ? { accountCredits: credits } : {}),
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
if (method === 'harnessmix/thread/command/execute') {
|
|
1000
|
+
if (params.arguments && Object.keys(params.arguments).length) throw new Error('This command does not accept arguments');
|
|
1001
|
+
const turn = await this.startNativeTurn(thread, '', undefined, params.commandId);
|
|
1002
|
+
return { accepted: true, turnId: turn.id };
|
|
1003
|
+
}
|
|
1004
|
+
if (method === 'harnessmix/thread/model/select') { await this.runtime.setModel(thread.id, await this.resolveModel(thread.harnessId, params.model)); return this.configuration(thread); }
|
|
1005
|
+
if (method === 'harnessmix/thread/thinking/select') { await this.runtime.setThinking(thread.id, params.thinkingOptionId); return this.configuration(thread); }
|
|
1006
|
+
if (method === 'harnessmix/thread/permission-mode/select') {
|
|
1007
|
+
await this.runtime.setOptions(thread.id, { permissionMode: params.permissionModeId });
|
|
1008
|
+
const session = this.runtime.sessions.get(thread.id);
|
|
1009
|
+
const adapter = this.runtime.adapters.get(thread.harnessId);
|
|
1010
|
+
if (session && adapter && typeof adapter.setPermissionMode === 'function') {
|
|
1011
|
+
try { await adapter.setPermissionMode(session, params.permissionModeId); } catch {}
|
|
1012
|
+
}
|
|
1013
|
+
return this.configuration(thread);
|
|
1014
|
+
}
|
|
1015
|
+
// 原地切换 Harness:会话历史保留,下条消息携带一次性上下文信封(/switch 指令的 RPC 等价物)
|
|
1016
|
+
if (method === 'harnessmix/thread/harness/switch') {
|
|
1017
|
+
const result = await this.runtime.switchHarness(thread.id, ALIASES[params.harnessId] || params.harnessId, {
|
|
1018
|
+
note: typeof params.note === 'string' ? params.note : undefined,
|
|
1019
|
+
intent: params.intent,
|
|
1020
|
+
includes: params.includes,
|
|
1021
|
+
});
|
|
1022
|
+
return { ...result, fromHarnessId: externalId(result.fromHarnessId), toHarnessId: externalId(result.toHarnessId) };
|
|
1023
|
+
}
|
|
1024
|
+
if (method === 'thread/fork' || method === 'harnessmix/thread/fork') {
|
|
1025
|
+
if (params.ephemeral || params.threadSource || params.excludeTurns) {
|
|
1026
|
+
throw new Error('Ephemeral fork is not supported');
|
|
1027
|
+
}
|
|
1028
|
+
const messageId = params.lastTurnId ? thread.messages.find(m => m.coreTurnId === params.lastTurnId)?.id : params.messageId;
|
|
1029
|
+
if (params.lastTurnId && !messageId) throw new Error('Unknown fork turn');
|
|
1030
|
+
const fork = await this.runtime.forkThread(thread.id, messageId);
|
|
1031
|
+
return method === 'harnessmix/thread/fork' ? { threadId: fork.id } : { thread: this.projectThread(fork) };
|
|
1032
|
+
}
|
|
1033
|
+
throw new Error(`Harness Mix native bridge does not support ${method} for this thread`);
|
|
1034
|
+
}
|
|
1035
|
+
onCore(event, projected) {
|
|
1036
|
+
const { threadId, turnId } = event;
|
|
1037
|
+
const item = projected.item;
|
|
1038
|
+
const notify = (method, extra) => this.emit({ method, params: { threadId, turnId, ...extra } });
|
|
1039
|
+
if (event.type === 'usage.updated') this.emit({ method: 'harnessmix/thread/usage/updated', params: { threadId, usage: projectUsage(projected.thread?.usage) } });
|
|
1040
|
+
if (projected.items) {
|
|
1041
|
+
for (const file of projected.items) this.onCore({ ...event, type: 'item.updated' }, { item: file });
|
|
1042
|
+
// 去重:内容未变(含初始空态的 3s 轮询)不重复推送,避免刷屏与页脚抖动;
|
|
1043
|
+
// 变化被完全还原时仍需推送一次空 diff 让 Desktop 清空页脚。
|
|
1044
|
+
const diffText = projected.items.map(i => projectItem(i)?.changes?.map(c => c.diff).filter(Boolean).join('\n')).filter(Boolean).join('\n');
|
|
1045
|
+
const diffKey = `${threadId}\0${turnId}`;
|
|
1046
|
+
const lastDiff = this.turnDiffs.get(diffKey);
|
|
1047
|
+
if (diffText !== lastDiff && (diffText || lastDiff !== undefined)) {
|
|
1048
|
+
if (this.turnDiffs.size > 200) this.turnDiffs.delete(this.turnDiffs.keys().next().value);
|
|
1049
|
+
this.turnDiffs.set(diffKey, diffText);
|
|
1050
|
+
notify('turn/diff/updated', { diff: diffText });
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
if (event.type === 'plan.updated' && item) notify('turn/plan/updated', { explanation: null, plan: (item.entries || []).map(e => ({ step: e.text || e.title || e.step, status: e.status === 'done' || e.status === 'completed' ? 'completed' : e.status === 'in_progress' ? 'inProgress' : 'pending' })) });
|
|
1054
|
+
if (event.type === 'turn.started') {
|
|
1055
|
+
notify('turn/started', { turn: this.turn(projected.turn) });
|
|
1056
|
+
// The Desktop sidebar spinner is driven by thread/status/changed, not by
|
|
1057
|
+
// turn lifecycle events alone — mirror the official app-server contract.
|
|
1058
|
+
notify('thread/status/changed', { status: { type: 'active', activeFlags: [] } });
|
|
1059
|
+
}
|
|
1060
|
+
if (item) {
|
|
1061
|
+
const converted = projectItem(item);
|
|
1062
|
+
if (converted) {
|
|
1063
|
+
const previous = this.published.get(item.id);
|
|
1064
|
+
if (!previous) notify('item/started', { item: converted, startedAtMs: item.createdAt ?? Date.now() });
|
|
1065
|
+
if (item.type === 'agent_message' && item.content?.length > (previous?.content?.length || 0)) notify('item/agentMessage/delta', { itemId: item.id, delta: item.content.slice(previous?.content?.length || 0) });
|
|
1066
|
+
if (item.type === 'reasoning' && item.content?.length > (previous?.content?.length || 0)) notify('item/reasoning/summaryTextDelta', { itemId: item.id, summaryIndex: 0, delta: item.content.slice(previous?.content?.length || 0) });
|
|
1067
|
+
if (terminal(item.status) && (!terminal(previous?.status) || ['file_change', 'tool_call'].includes(item.type) && JSON.stringify(converted) !== JSON.stringify(projectItem(previous)))) notify('item/completed', { item: converted, completedAtMs: item.updatedAt ?? item.createdAt ?? Date.now() });
|
|
1068
|
+
this.published.set(item.id, structuredClone(item));
|
|
1069
|
+
if (this.published.size > 200) {
|
|
1070
|
+
const oldest = this.published.keys().next().value;
|
|
1071
|
+
if (oldest) this.published.delete(oldest);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
if (['approval', 'question'].includes(item.type) && event.type === 'item.started') {
|
|
1075
|
+
const id = `harness-mix:approval:${item.id}`;
|
|
1076
|
+
this.approvals.set(id, { threadId, requestId: item.requestId, item });
|
|
1077
|
+
const question = item.type === 'question' || item.method === 'select';
|
|
1078
|
+
this.emit({ id, method: question ? 'item/tool/requestUserInput' : 'item/commandExecution/requestApproval', params: {
|
|
1079
|
+
threadId, turnId, itemId: item.id,
|
|
1080
|
+
...(question ? { questions: [{ id: item.requestId, header: 'Harness', question: item.message || item.title || 'Native Harness input', isOther: true, isSecret: false,
|
|
1081
|
+
options: item.options?.map(o => ({ label: typeof o === 'string' ? o : o.label || o.id, description: typeof o === 'string' ? o : o.description || o.label || o.id })) || null }] }
|
|
1082
|
+
: { reason: item.message || item.title, command: null, cwd: this.thread(threadId)?.cwd, availableDecisions: ['accept', 'decline'] }) } });
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
if (event.type === 'turn.waiting') notify('thread/status/changed', { status: { type: 'active', activeFlags: [] } });
|
|
1086
|
+
if (event.type === 'turn.resumed') notify('thread/status/changed', { status: { type: 'active', activeFlags: [] } });
|
|
1087
|
+
if (event.type.startsWith('turn.') && projected.turn && terminal(projected.turn.status)) {
|
|
1088
|
+
notify('turn/completed', { turn: this.turn(projected.turn) });
|
|
1089
|
+
notify('thread/status/changed', { status: { type: 'idle' } });
|
|
1090
|
+
if (Array.isArray(projected.turn.itemIds)) {
|
|
1091
|
+
for (const id of projected.turn.itemIds) this.published.delete(id);
|
|
1092
|
+
}
|
|
1093
|
+
for (const [id, app] of this.approvals.entries()) {
|
|
1094
|
+
if (app.threadId === threadId) this.approvals.delete(id);
|
|
1095
|
+
}
|
|
1096
|
+
if (projected.turn.status === 'completed') {
|
|
1097
|
+
setTimeout(async () => {
|
|
1098
|
+
const queue = this.getQueue(threadId);
|
|
1099
|
+
const currentThread = this.thread(threadId);
|
|
1100
|
+
if (queue.length > 0 && currentThread && !this.runtime.execution.isRunning(threadId) && !this.steering.has(threadId) && !this.runtime.sending?.has(threadId)) {
|
|
1101
|
+
try {
|
|
1102
|
+
await this.startQueuedSubmission(currentThread, queue[0]);
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
console.error(`Failed to auto-drain queued submission for thread ${threadId}:`, err);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}, 2500);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
async startQueuedSubmission(thread, submission) {
|
|
1112
|
+
if (this.queueStarts.has(submission.id)) throw new Error('Queued submission is already starting');
|
|
1113
|
+
this.queueStarts.add(submission.id);
|
|
1114
|
+
try {
|
|
1115
|
+
// Keep the item addressable until the native harness has accepted a real Turn. If
|
|
1116
|
+
// input preparation or turn startup fails, Desktop can still edit/retry/delete it.
|
|
1117
|
+
const { text, attachments } = await prepareInput(submission.input, thread.cwd);
|
|
1118
|
+
if (attachments.length && !this.runtime.getCapabilities(thread.harnessId).conversation.attachments) {
|
|
1119
|
+
throw new Error('当前 Harness 不支持图片附件');
|
|
1120
|
+
}
|
|
1121
|
+
let started;
|
|
1122
|
+
if (this.runtime.execution.isRunning(thread.id)) {
|
|
1123
|
+
const turnId = await this.steerExclusive(thread, thread.currentTurn?.id, text, attachments);
|
|
1124
|
+
started = this.runtime.core.getTurn(turnId) || thread.currentTurn;
|
|
1125
|
+
} else {
|
|
1126
|
+
started = await this.startNativeTurn(thread, text, attachments);
|
|
1127
|
+
}
|
|
1128
|
+
const queue = this.getQueue(thread.id);
|
|
1129
|
+
const idx = queue.findIndex(item => item.id === submission.id);
|
|
1130
|
+
if (idx !== -1) {
|
|
1131
|
+
queue.splice(idx, 1);
|
|
1132
|
+
this.emitQueueChanged(thread.id);
|
|
1133
|
+
}
|
|
1134
|
+
return started;
|
|
1135
|
+
} finally {
|
|
1136
|
+
this.queueStarts.delete(submission.id);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
async startNativeTurn(thread, text, attachments, commandId, turnPermissions) {
|
|
1140
|
+
const before = thread.currentTurn?.id;
|
|
1141
|
+
for (let i = 0; i < 100 && this.runtime.sending?.has(thread.id) && !this.runtime.execution.isRunning(thread.id); i++) {
|
|
1142
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
1143
|
+
}
|
|
1144
|
+
let failure;
|
|
1145
|
+
const running = (commandId ? this.runtime.executeCommand(thread.id, commandId) : this.runtime.send(thread.id, text, { attachments, turnPermissions })).catch(error => { failure = error; });
|
|
1146
|
+
for (let attempt = 0; attempt < 600 && thread.currentTurn?.id === before && !failure; attempt++) await new Promise(resolve => setTimeout(resolve, 50));
|
|
1147
|
+
if (failure) throw failure;
|
|
1148
|
+
if (!thread.currentTurn || thread.currentTurn.id === before) throw new Error('Native turn did not start');
|
|
1149
|
+
void running;
|
|
1150
|
+
return thread.currentTurn;
|
|
1151
|
+
}
|
|
1152
|
+
async steerThread(thread, { expectedTurnId, text, attachments, messageKey, fingerprint }) {
|
|
1153
|
+
if (messageKey) {
|
|
1154
|
+
const receipt = this.steerReceipts.get(messageKey);
|
|
1155
|
+
// Outcome-unknown retry: identical payload returns the original receipt; a
|
|
1156
|
+
// conflicting payload under the same message identity is rejected.
|
|
1157
|
+
if (receipt) {
|
|
1158
|
+
if (receipt.fingerprint !== fingerprint) throw new Error('Conflicting steering payload for the same message');
|
|
1159
|
+
return { turnId: await receipt.promise };
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
if (this.steering.has(thread.id)) throw new Error('This Thread is already changing direction');
|
|
1163
|
+
const work = this.steerExclusive(thread, expectedTurnId, text, attachments);
|
|
1164
|
+
this.steering.set(thread.id, work);
|
|
1165
|
+
if (messageKey) {
|
|
1166
|
+
if (this.steerReceipts.size >= 200) this.steerReceipts.delete(this.steerReceipts.keys().next().value);
|
|
1167
|
+
this.steerReceipts.set(messageKey, { fingerprint, promise: work });
|
|
1168
|
+
}
|
|
1169
|
+
try {
|
|
1170
|
+
return { turnId: await work };
|
|
1171
|
+
} finally {
|
|
1172
|
+
this.steering.delete(thread.id);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
async steerExclusive(thread, expectedTurnId, text, attachments) {
|
|
1176
|
+
const active = this.runtime.execution.isRunning(thread.id) ? thread.currentTurn : null;
|
|
1177
|
+
if (active) {
|
|
1178
|
+
if (active.id !== expectedTurnId) throw new Error('The active Turn no longer matches the steering target');
|
|
1179
|
+
await this.runtime.cancel(thread.id);
|
|
1180
|
+
// Cancel settles synchronously in Core, but the acknowledgement is not completion:
|
|
1181
|
+
// the replacement waits until the old Turn is fully terminal and the file-review
|
|
1182
|
+
// snapshot has settled (≤20s, shorter than the Desktop submission timeout).
|
|
1183
|
+
const settled = () => !this.runtime.execution.isRunning(thread.id) && !thread.reviewPending && !this.runtime.sending?.has(thread.id);
|
|
1184
|
+
const deadline = Date.now() + 20_000;
|
|
1185
|
+
while (!settled() && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 50));
|
|
1186
|
+
if (!settled()) throw new Error('Timed out waiting for the previous Turn to settle');
|
|
1187
|
+
} else {
|
|
1188
|
+
const last = thread.currentTurn;
|
|
1189
|
+
// A later Turn already exists, or the target was never this Thread's last Turn: stale.
|
|
1190
|
+
if (last && last.id !== expectedTurnId) throw new Error('The active Turn no longer matches the steering target');
|
|
1191
|
+
}
|
|
1192
|
+
// If a native Turn started on its own in the meantime, send() refuses and the new
|
|
1193
|
+
// Turn is left running; a failed replacement never cancels unexpected work.
|
|
1194
|
+
const started = await this.startNativeTurn(thread, text, attachments);
|
|
1195
|
+
return started.id;
|
|
1196
|
+
}
|
|
1197
|
+
async respond(message) {
|
|
1198
|
+
const pending = this.approvals.get(message.id);
|
|
1199
|
+
if (!pending) return false;
|
|
1200
|
+
if (message.error) throw new Error(message.error.message || 'Approval UI error');
|
|
1201
|
+
// Desktop 的 answers 为 string[]:多选提问(如 ACP/OpenCode multiple)勾选多项时
|
|
1202
|
+
// 只取 [0] 会无声吞掉其余选项,且 opencode 对 multiple 题执行 JSON.parse(response.value),
|
|
1203
|
+
// 单值字符串会直接抛 SyntaxError——多答案 JSON 编码全量保留,单答案维持原字符串。
|
|
1204
|
+
const rawAnswers = message.result?.answers?.[pending.requestId]?.answers;
|
|
1205
|
+
const answer = Array.isArray(rawAnswers) && rawAnswers.length > 1 ? JSON.stringify(rawAnswers) : (rawAnswers?.[0] ?? '');
|
|
1206
|
+
const decision = message.result?.decision;
|
|
1207
|
+
const response = pending.item.type === 'question' ? { value: answer || '' } : pending.item.method === 'select' ? { value: answer || '' } : { confirmed: decision === 'accept' || decision === 'acceptForSession' };
|
|
1208
|
+
await this.runtime.respondApproval(pending.threadId, pending.requestId, response);
|
|
1209
|
+
this.approvals.delete(message.id);
|
|
1210
|
+
return true;
|
|
1211
|
+
}
|
|
1212
|
+
close() {
|
|
1213
|
+
this.closed = true;
|
|
1214
|
+
this.codexAccounts.close();
|
|
1215
|
+
this.unsubscribe();
|
|
1216
|
+
this.unsubscribeRuntime();
|
|
1217
|
+
for (const timer of this.queueNotifications.values()) clearTimeout(timer);
|
|
1218
|
+
this.queueNotifications.clear();
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
module.exports = { NativeProtocol, decodeRoute, projectItem, externalId, routeModel };
|