@tea-agent/loop-agent 0.43.0-next.3 → 0.43.0-next.5
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 +81 -9
- package/dist/application/task-lifecycle/advance.js +17 -4
- package/dist/application/task-lifecycle/observe.js +43 -29
- package/dist/application/task-lifecycle/plan-transitions.js +5 -4
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +244 -33
- package/dist/executors/pi-executor.js +2 -0
- package/dist/executors/pi-sdk-executor.js +75 -39
- package/dist/task/source-prepare/source-fidelity-pi.js +2 -2
- package/dist/task/source-references.js +48 -23
- package/dist/worker/console/chat/assistant-content.js +23 -2
- package/dist/worker/console/chat/explore-tools.js +13 -0
- package/dist/worker/console/chat/pi-runtime.js +131 -95
- package/dist/worker/console/chat/routes.js +175 -54
- package/dist/worker/console/chat/scm-routes.js +193 -0
- package/dist/worker/console/chat/scm-service.js +253 -0
- package/dist/worker/console/chat/scm-tools.js +111 -0
- package/dist/worker/console/chat/sdd-data-alignment.js +65 -9
- package/dist/worker/console/chat/session-catalog.js +1 -0
- package/dist/worker/console/chat/session-mode-view.js +48 -0
- package/dist/worker/console/chat/session-mode.js +218 -0
- package/dist/worker/console/chat/session-store.js +22 -6
- package/dist/worker/console/chat/shortcuts.js +6 -0
- package/dist/worker/console/chat/terminal-routes.js +216 -0
- package/dist/worker/console/chat/terminal-sessions.js +284 -0
- package/dist/worker/console/chat/terminal-tools.js +199 -0
- package/dist/worker/console/chat/tool-preview.js +11 -0
- package/dist/worker/console/chat/tools.js +2 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-hUmM_GRf.js → abnfDiagram-N423BO3Z-BuyrlONK.js} +1 -1
- package/dist/worker/console/static/assets/{arc-Bt-xuieW.js → arc-BvCl_wpi.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DTqkL4DJ.js → architectureDiagram-T3A2C74G-BkBpBaRq.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Cy0MNnzV.js → blockDiagram-VBNYF7ZC-yve4Mcb_.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DDdlT0Pd.js → c4Diagram-5PPSVZJV-9yLGboBx.js} +1 -1
- package/dist/worker/console/static/assets/channel-BRahIhjw.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-3JawwNZ6.js → chunk-2GRJ4B5K-BKP69b8r.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DoTp7Cri.js → chunk-2Q5K7J3B-NWFRnXju.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CcirhZaS.js → chunk-5RXB4S5H-BiweqLEc.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-VrG21gnP.js → chunk-5VM5RSS4-CP9GHmRY.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DDOgIS70.js → chunk-6Q2QTUOP-BiGDkBtj.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CBjmgvJ_.js → chunk-GF5L2VYU-DkJ4ZjQS.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CCQSht8M.js → chunk-JWPE2WC7-B5jjn7zq.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BpGMkUUV.js → chunk-KBJHAD2P-C6KoK9O0.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DNDT41QL.js → chunk-RYQCIY6F-BvNIfyn-.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-euXnDFbn.js → chunk-XXDRQBXY-BZ33iX4W.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-GOC80mhq.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-GOC80mhq.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BJ6NAeql.js → cose-bilkent-JH36ORCC-CP8eIuZd.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-F5JKIvIq.js → cynefin-VYW2F7L2-DZECjpOe.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Cwmkl_Gi.js → cynefinDiagram-MW4NZA55-CvVJoFqf.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BmCZ8815.js → dagre-VZM6K2ZE-D_JTqdAn.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zWA3hUnX.js → diagram-7IWD3JNH-Db7AQ0z0.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BzA-888F.js → diagram-B4RE2ZJO-AZPVo5CZ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-tPNheoch.js → diagram-LBJQPF4R-BgXqI3E6.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Cdi7Fyqp.js → diagram-Q27KOJAE-BYjFn3Ba.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-DFK9VBf1.js → diagram-UB23O5K3-B4rJgGGO.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-xCUr1kpg.js → ebnfDiagram-BXEA7PRR-C6XWZhpw.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BANQHY_A.js → erDiagram-JOGREHBK-1YdQRIQW.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-VHJYEMkP.js → flowDiagram-UKHOOZJN-l3a0AwN0.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiyZQ0C6.js → ganttDiagram-PKOTCBZU-CQL96F2Z.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CZtIeBBa.js → gitGraphDiagram-DS77QQ5N-BxCvafRb.js} +1 -1
- package/dist/worker/console/static/assets/index-4wCGsXuU.js +465 -0
- package/dist/worker/console/static/assets/index-CiF9WBoR.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BuSspC0X.js → infoDiagram-6WML65LV-BUipIeDO.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DRJZyPFy.js → ishikawaDiagram-WSZJBQD7-CRfomVMX.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DIfXIVuE.js → journeyDiagram-NVQOT4AX-_UFvyviX.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CT10_7Lt.js → kanban-definition-27J2QSJJ-CKIRoqKv.js} +1 -1
- package/dist/worker/console/static/assets/{linear-ufemj74H.js → linear-CjttJDCM.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-DEQe8-_m.js → mermaid.core-DPvHX155.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BRLUrbrC.js → mindmap-definition-FAOFIHXS-Qmj-qwBe.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BgibKtiF.js → pegDiagram-VL7TDLO6-Auw6DRgq.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-D3eyfdV9.js → pieDiagram-7S7Q4E2Y-CqMrfD4c.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DXeTk5Px.js → quadrantDiagram-CIZ2JOQS-BAddeggH.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BgXdFEe7.js → railroadDiagram-AXF67PYL-DG1JuOu2.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CseC2A7R.js → requirementDiagram-LRYGKXZP-DLYGMKlC.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-D80PFzfJ.js → sankeyDiagram-W5VNT64P-g68SbLM9.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BWzHW7LS.js → sequenceDiagram-SI44F4Z6-CmszLKv8.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-xWQYTLAn.js → sizeCapture-X5ZJPWSS-D2tEKUCj.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CLxtpZeL.js → stateDiagram-OKZ733FA-Syc5E_Jf.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CwWwuPI-.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DwugtmWV.js → swimlanes-SLNWSIFB-Ibn0IRCa.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-A7fqemr6.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DHEOwHIe.js → timeline-definition-Z64GVDOM-2ZW9W3iS.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-pmbZKlBK.js → vennDiagram-T6HMQDX7-v50HsKUh.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CivHoM5q.js → wardleyDiagram-T6FBY63Y-DqCXEoDH.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BfHIE7o6.js → xychartDiagram-ELKLHX3M-vIP8Vo9d.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +122 -10
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +27 -15
- package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
- package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +20 -4
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +18 -2
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +39 -4
- package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -7
- package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
- package/dist/worker/console/workspace-context.js +15 -1
- package/dist/workflows/dag/frontend-capacity.js +9 -0
- package/dist/workflows/dag/frontend-design-policy.js +2 -1
- package/dist/workflows/dag/frontend-shape-facts.js +12 -2
- package/package.json +6 -3
- package/skills/frontend-plan/SKILL.md +7 -0
- package/dist/worker/console/static/assets/channel-BpzRnZmU.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BhlMVqCc.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BhlMVqCc.js +0 -1
- package/dist/worker/console/static/assets/index-BlN6ytLr.css +0 -1
- package/dist/worker/console/static/assets/index-CA-jfWlE.js +0 -449
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CiVoq4G-.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BkdgdpfR.js +0 -8
|
@@ -16,6 +16,7 @@ import { randomUUID } from "node:crypto";
|
|
|
16
16
|
import { access } from "node:fs/promises";
|
|
17
17
|
import { parseAssistantContent } from "./assistant-content.js";
|
|
18
18
|
import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_DENIED_OPERATOR_ACTIONS, } from "./tools.js";
|
|
19
|
+
import { InvalidChatSessionModeError, SESSION_MODE_ACTION_DENIED_CODE, SESSION_MODE_ACTION_DENIED_MESSAGE, SESSION_MODE_IMMUTABLE_CODE, UnknownChatSessionModeError, isWorkflowSessionMode, parseCreateSessionMode, readStoredSessionMode, shouldAcceptOperatorSnapshot, } from "./session-mode.js";
|
|
19
20
|
import { buildModelCallableToolSchemas } from "./tool-adapter.js";
|
|
20
21
|
import { cleanChatTitle, acceptedOperationIdFromResult, isBlankChatSession } from "./session-store.js";
|
|
21
22
|
import { hashChatTurnPayload, projectChatToolEventPayload, projectCompactSnapshot, } from "./chat-event-store.js";
|
|
@@ -44,11 +45,79 @@ import { buildGoalCompactionInstructions, buildGoalContinuationContext, } from "
|
|
|
44
45
|
import { classifyGoalRoundAdmission, } from "./goal-round-driver.js";
|
|
45
46
|
import { isTerminalOperationState } from "../../../infrastructure/console/operation-store.js";
|
|
46
47
|
import { SddDataAlignmentError, } from "./sdd-data-alignment.js";
|
|
48
|
+
import { TerminalSessionRegistry } from "./terminal-sessions.js";
|
|
49
|
+
import { handleTerminalRequest } from "./terminal-routes.js";
|
|
50
|
+
import { handleScmRequest } from "./scm-routes.js";
|
|
47
51
|
function projectBrowserChatSession(record) {
|
|
48
52
|
const { telemetrySessionId, ...projected } = record;
|
|
49
53
|
void telemetrySessionId;
|
|
50
54
|
return projected;
|
|
51
55
|
}
|
|
56
|
+
function reopenInitFromRecord(record) {
|
|
57
|
+
return {
|
|
58
|
+
sessionId: record.sessionId,
|
|
59
|
+
sessionFile: record.sessionFile,
|
|
60
|
+
...(record.modelProvider && record.modelId
|
|
61
|
+
? { model: { provider: record.modelProvider, modelId: record.modelId } }
|
|
62
|
+
: {}),
|
|
63
|
+
sessionMode: record.sessionMode,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async function readChatSessionMode(deps, sessionId) {
|
|
67
|
+
let record;
|
|
68
|
+
try {
|
|
69
|
+
record = await deps.store.get(sessionId);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error instanceof UnknownChatSessionModeError) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
status: 409,
|
|
76
|
+
code: error.code,
|
|
77
|
+
message: error.message,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
if (!record) {
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
status: 404,
|
|
86
|
+
code: "NOT_FOUND",
|
|
87
|
+
message: `chat session not found: ${sessionId}`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// Normalize: the store contract always fills sessionMode, but handler-level
|
|
91
|
+
// fakes / partial records may omit it — treat missing as workflow (the
|
|
92
|
+
// stock-record read rule), while a present-but-unknown value still fails
|
|
93
|
+
// closed in the store layer.
|
|
94
|
+
return {
|
|
95
|
+
ok: true,
|
|
96
|
+
record,
|
|
97
|
+
sessionMode: readStoredSessionMode(record.sessionMode),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
async function requireWorkflowSession(res, deps, sessionId) {
|
|
101
|
+
const resolved = await readChatSessionMode(deps, sessionId);
|
|
102
|
+
if (!resolved.ok) {
|
|
103
|
+
sendJson(res, resolved.status, {
|
|
104
|
+
ok: false,
|
|
105
|
+
error: { code: resolved.code, message: resolved.message },
|
|
106
|
+
});
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
if (!isWorkflowSessionMode(resolved.sessionMode)) {
|
|
110
|
+
sendJson(res, 409, {
|
|
111
|
+
ok: false,
|
|
112
|
+
error: {
|
|
113
|
+
code: SESSION_MODE_ACTION_DENIED_CODE,
|
|
114
|
+
message: SESSION_MODE_ACTION_DENIED_MESSAGE,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
return resolved.record;
|
|
120
|
+
}
|
|
52
121
|
/**
|
|
53
122
|
* Reattach the durable event-ring order to session-store rows. The session
|
|
54
123
|
* store remains the content authority; the ring only supplies chronology.
|
|
@@ -565,6 +634,22 @@ workspaceId) {
|
|
|
565
634
|
});
|
|
566
635
|
return;
|
|
567
636
|
}
|
|
637
|
+
let sessionMode;
|
|
638
|
+
try {
|
|
639
|
+
sessionMode = parseCreateSessionMode(body.sessionMode);
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
sendJson(res, 400, {
|
|
643
|
+
ok: false,
|
|
644
|
+
error: {
|
|
645
|
+
code: error instanceof InvalidChatSessionModeError
|
|
646
|
+
? error.code
|
|
647
|
+
: "INVALID_SESSION_MODE",
|
|
648
|
+
message: error instanceof Error ? error.message : String(error),
|
|
649
|
+
},
|
|
650
|
+
});
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
568
653
|
const provider = typeof body.modelProvider === "string" ? body.modelProvider : undefined;
|
|
569
654
|
const modelId = typeof body.modelId === "string" ? body.modelId : undefined;
|
|
570
655
|
const snapshotInput = body.snapshot;
|
|
@@ -586,6 +671,16 @@ workspaceId) {
|
|
|
586
671
|
let createdSessionId;
|
|
587
672
|
let persisted = false;
|
|
588
673
|
try {
|
|
674
|
+
if (snapshotInput !== undefined && !shouldAcceptOperatorSnapshot(sessionMode)) {
|
|
675
|
+
sendJson(res, 409, {
|
|
676
|
+
ok: false,
|
|
677
|
+
error: {
|
|
678
|
+
code: SESSION_MODE_ACTION_DENIED_CODE,
|
|
679
|
+
message: "Operator snapshots cannot be imported into Pi sessions",
|
|
680
|
+
},
|
|
681
|
+
});
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
589
684
|
const snapshot = snapshotInput === undefined ? undefined : projectCompactSnapshot(snapshotInput);
|
|
590
685
|
// A Console record promises that its Pi JSONL can be reopened after a
|
|
591
686
|
// Console restart. Do not make that promise until Pi has finished
|
|
@@ -593,6 +688,7 @@ workspaceId) {
|
|
|
593
688
|
// future JSONL path, leaving an unrecoverable orphan when init failed or
|
|
594
689
|
// the process restarted in that window.
|
|
595
690
|
const handle = await deps.runtime.createSession({
|
|
691
|
+
sessionMode,
|
|
596
692
|
...(provider && modelId ? { model: { provider, modelId } } : {}),
|
|
597
693
|
...(snapshot
|
|
598
694
|
? {
|
|
@@ -619,6 +715,7 @@ workspaceId) {
|
|
|
619
715
|
sessionFile: handle.sessionFile,
|
|
620
716
|
modelProvider: selection.model?.provider ?? handle.model?.provider,
|
|
621
717
|
modelId: selection.model?.modelId ?? handle.model?.modelId,
|
|
718
|
+
sessionMode,
|
|
622
719
|
// AC-R7-002: explicit parameter from the dispatcher — the runtime
|
|
623
720
|
// store.create wrapper is gone; body workspaceId (if any) only acts as
|
|
624
721
|
// a fallback for direct (non-dispatcher) handler tests.
|
|
@@ -689,6 +786,7 @@ export async function handleForkChatSession(req, res, deps, sourceSessionId) {
|
|
|
689
786
|
try {
|
|
690
787
|
const handle = await deps.runtime.forkSession({
|
|
691
788
|
sourceSessionFile: source.sessionFile ?? "",
|
|
789
|
+
sessionMode: source.sessionMode,
|
|
692
790
|
...(source.modelProvider && source.modelId
|
|
693
791
|
? { model: { provider: source.modelProvider, modelId: source.modelId } }
|
|
694
792
|
: {}),
|
|
@@ -755,10 +853,7 @@ export async function handleBranchChatSession(req, res, deps, sessionId) {
|
|
|
755
853
|
}
|
|
756
854
|
try {
|
|
757
855
|
if (!deps.runtime.hasSession(sessionId)) {
|
|
758
|
-
const reopened = await deps.runtime.reopenSession(
|
|
759
|
-
sessionId,
|
|
760
|
-
sessionFile: record.sessionFile,
|
|
761
|
-
});
|
|
856
|
+
const reopened = await deps.runtime.reopenSession(reopenInitFromRecord(record));
|
|
762
857
|
if (!reopened.ok) {
|
|
763
858
|
const mapped = await mapFailedReopen(deps, sessionId, reopened);
|
|
764
859
|
sendJson(res, mapped.status, {
|
|
@@ -1231,13 +1326,8 @@ export async function handleChatGoal(req, res, deps, sessionId) {
|
|
|
1231
1326
|
});
|
|
1232
1327
|
return;
|
|
1233
1328
|
}
|
|
1234
|
-
if (!(await deps
|
|
1235
|
-
sendJson(res, 404, {
|
|
1236
|
-
ok: false,
|
|
1237
|
-
error: { code: "NOT_FOUND", message: `chat session not found: ${sessionId}` },
|
|
1238
|
-
});
|
|
1329
|
+
if (!(await requireWorkflowSession(res, deps, sessionId)))
|
|
1239
1330
|
return;
|
|
1240
|
-
}
|
|
1241
1331
|
if (req.method === "GET") {
|
|
1242
1332
|
sendJson(res, 200, { ok: true, goal: registry.get(sessionId) });
|
|
1243
1333
|
return;
|
|
@@ -1451,6 +1541,7 @@ export async function listChatSessionSummaries(deps, ownerWorkspaceId) {
|
|
|
1451
1541
|
...(session.thinkingLevel !== undefined
|
|
1452
1542
|
? { thinkingLevel: session.thinkingLevel }
|
|
1453
1543
|
: {}),
|
|
1544
|
+
sessionMode: session.sessionMode,
|
|
1454
1545
|
activeTurnId: active?.turnId,
|
|
1455
1546
|
turnPhase: active?.abortRequestedAt
|
|
1456
1547
|
? "stopping"
|
|
@@ -1474,6 +1565,16 @@ export async function handlePatchChatSession(req, res, deps, sessionId) {
|
|
|
1474
1565
|
}
|
|
1475
1566
|
try {
|
|
1476
1567
|
const body = await readJsonBody(req);
|
|
1568
|
+
if ("sessionMode" in body) {
|
|
1569
|
+
sendJson(res, 409, {
|
|
1570
|
+
ok: false,
|
|
1571
|
+
error: {
|
|
1572
|
+
code: SESSION_MODE_IMMUTABLE_CODE,
|
|
1573
|
+
message: "sessionMode cannot be changed after create",
|
|
1574
|
+
},
|
|
1575
|
+
});
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1477
1578
|
let record;
|
|
1478
1579
|
if (typeof body.title === "string")
|
|
1479
1580
|
record = await deps.store.rename(sessionId, body.title);
|
|
@@ -1532,13 +1633,7 @@ export async function handleReopenChatSession(req, res, deps, sessionId) {
|
|
|
1532
1633
|
});
|
|
1533
1634
|
return;
|
|
1534
1635
|
}
|
|
1535
|
-
const result = await deps.runtime.reopenSession(
|
|
1536
|
-
sessionId,
|
|
1537
|
-
sessionFile: record.sessionFile,
|
|
1538
|
-
...(record.modelProvider && record.modelId
|
|
1539
|
-
? { model: { provider: record.modelProvider, modelId: record.modelId } }
|
|
1540
|
-
: {}),
|
|
1541
|
-
});
|
|
1636
|
+
const result = await deps.runtime.reopenSession(reopenInitFromRecord(record));
|
|
1542
1637
|
if (!result.ok) {
|
|
1543
1638
|
const mapped = await mapFailedReopen(deps, sessionId, result);
|
|
1544
1639
|
sendJson(res, mapped.status, {
|
|
@@ -1618,13 +1713,7 @@ export async function handleChatPrompt(req, res, deps, sessionId) {
|
|
|
1618
1713
|
});
|
|
1619
1714
|
return;
|
|
1620
1715
|
}
|
|
1621
|
-
const reopen = await deps.runtime.reopenSession(
|
|
1622
|
-
sessionId,
|
|
1623
|
-
sessionFile: record.sessionFile,
|
|
1624
|
-
...(record.modelProvider && record.modelId
|
|
1625
|
-
? { model: { provider: record.modelProvider, modelId: record.modelId } }
|
|
1626
|
-
: {}),
|
|
1627
|
-
});
|
|
1716
|
+
const reopen = await deps.runtime.reopenSession(reopenInitFromRecord(record));
|
|
1628
1717
|
if (!reopen.ok) {
|
|
1629
1718
|
const mapped = await mapFailedReopen(deps, sessionId, reopen);
|
|
1630
1719
|
sendJson(res, mapped.status, {
|
|
@@ -1888,13 +1977,7 @@ export async function handleCreateChatTurn(req, res, deps, sessionId, trustedPay
|
|
|
1888
1977
|
});
|
|
1889
1978
|
return;
|
|
1890
1979
|
}
|
|
1891
|
-
const reopen = await deps.runtime.reopenSession(
|
|
1892
|
-
sessionId,
|
|
1893
|
-
sessionFile: record.sessionFile,
|
|
1894
|
-
...(record.modelProvider && record.modelId
|
|
1895
|
-
? { model: { provider: record.modelProvider, modelId: record.modelId } }
|
|
1896
|
-
: {}),
|
|
1897
|
-
});
|
|
1980
|
+
const reopen = await deps.runtime.reopenSession(reopenInitFromRecord(record));
|
|
1898
1981
|
if (!reopen.ok) {
|
|
1899
1982
|
const mapped = await mapFailedReopen(deps, sessionId, reopen);
|
|
1900
1983
|
sendJson(res, mapped.status, {
|
|
@@ -2172,6 +2255,10 @@ export async function driveGoalRound(deps, sessionId) {
|
|
|
2172
2255
|
registry.disarm(sessionId);
|
|
2173
2256
|
return "idle";
|
|
2174
2257
|
}
|
|
2258
|
+
if (!isWorkflowSessionMode(readStoredSessionMode(record.sessionMode))) {
|
|
2259
|
+
registry.disarm(sessionId);
|
|
2260
|
+
return "idle";
|
|
2261
|
+
}
|
|
2175
2262
|
const runtimeFacts = deps.runtime.getSessionBusyFacts(sessionId);
|
|
2176
2263
|
const operationStore = deps.runtime.actionContext?.operations;
|
|
2177
2264
|
const linkedOperationIds = linkedOperationIdsForGoal(deps.events.snapshot(sessionId));
|
|
@@ -2820,6 +2907,7 @@ export async function handleChatCommands(res, deps, sessionId) {
|
|
|
2820
2907
|
const warning = typeof listed.warning === "string" && listed.warning.trim()
|
|
2821
2908
|
? listed.warning.trim().slice(0, 160)
|
|
2822
2909
|
: undefined;
|
|
2910
|
+
const resolvedMode = await readChatSessionMode(deps, sessionId);
|
|
2823
2911
|
res.statusCode = 200;
|
|
2824
2912
|
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
2825
2913
|
res.end(JSON.stringify({
|
|
@@ -2827,18 +2915,25 @@ export async function handleChatCommands(res, deps, sessionId) {
|
|
|
2827
2915
|
commands,
|
|
2828
2916
|
capabilities: {
|
|
2829
2917
|
sddBingo: Boolean(await deps.sddDataAlignment?.isEligible()),
|
|
2918
|
+
...(resolvedMode.ok ? { sessionMode: resolvedMode.sessionMode } : {}),
|
|
2830
2919
|
},
|
|
2831
2920
|
...(warning ? { warning } : {}),
|
|
2832
2921
|
}));
|
|
2833
2922
|
}
|
|
2834
2923
|
catch (error) {
|
|
2835
2924
|
// Whole-list / readiness failures stay fail-soft with empty commands + warning.
|
|
2925
|
+
// Keep the two capability shapes symmetric: the fallback mode mirrors the
|
|
2926
|
+
// client-side prop fallback in useComposer.
|
|
2927
|
+
const fallbackMode = await readChatSessionMode(deps, sessionId).catch(() => undefined);
|
|
2836
2928
|
res.statusCode = 200;
|
|
2837
2929
|
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
2838
2930
|
res.end(JSON.stringify({
|
|
2839
2931
|
ok: true,
|
|
2840
2932
|
commands: [],
|
|
2841
|
-
capabilities: {
|
|
2933
|
+
capabilities: {
|
|
2934
|
+
sddBingo: false,
|
|
2935
|
+
...(fallbackMode?.ok ? { sessionMode: fallbackMode.sessionMode } : {}),
|
|
2936
|
+
},
|
|
2842
2937
|
warning: error instanceof Error ? error.message : "commands unavailable",
|
|
2843
2938
|
}));
|
|
2844
2939
|
}
|
|
@@ -3083,13 +3178,7 @@ export async function ensureActiveSession(deps, sessionId) {
|
|
|
3083
3178
|
message: `chat session not found: ${sessionId}`,
|
|
3084
3179
|
};
|
|
3085
3180
|
}
|
|
3086
|
-
const reopened = await deps.runtime.reopenSession(
|
|
3087
|
-
sessionId,
|
|
3088
|
-
sessionFile: record.sessionFile,
|
|
3089
|
-
...(record.modelProvider && record.modelId
|
|
3090
|
-
? { model: { provider: record.modelProvider, modelId: record.modelId } }
|
|
3091
|
-
: {}),
|
|
3092
|
-
});
|
|
3181
|
+
const reopened = await deps.runtime.reopenSession(reopenInitFromRecord(record));
|
|
3093
3182
|
if (!reopened.ok) {
|
|
3094
3183
|
return { ok: false, ...(await mapFailedReopen(deps, sessionId, reopened)) };
|
|
3095
3184
|
}
|
|
@@ -3418,7 +3507,7 @@ async function handleStartInterview(req, res, deps, sessionId) {
|
|
|
3418
3507
|
});
|
|
3419
3508
|
return;
|
|
3420
3509
|
}
|
|
3421
|
-
if (!(await
|
|
3510
|
+
if (!(await requireWorkflowSession(res, deps, sessionId)))
|
|
3422
3511
|
return;
|
|
3423
3512
|
try {
|
|
3424
3513
|
const body = await readJsonBody(req);
|
|
@@ -3460,7 +3549,7 @@ async function handleAnswerInterview(req, res, deps, sessionId, interviewSession
|
|
|
3460
3549
|
});
|
|
3461
3550
|
return;
|
|
3462
3551
|
}
|
|
3463
|
-
if (!(await
|
|
3552
|
+
if (!(await requireWorkflowSession(res, deps, sessionId)))
|
|
3464
3553
|
return;
|
|
3465
3554
|
try {
|
|
3466
3555
|
const body = await readJsonBody(req);
|
|
@@ -3496,7 +3585,7 @@ async function handleAnswerInterview(req, res, deps, sessionId, interviewSession
|
|
|
3496
3585
|
}
|
|
3497
3586
|
}
|
|
3498
3587
|
async function handleGetInterviewFact(res, deps, sessionId, interviewSessionId, fact) {
|
|
3499
|
-
if (!(await
|
|
3588
|
+
if (!(await requireWorkflowSession(res, deps, sessionId)))
|
|
3500
3589
|
return;
|
|
3501
3590
|
const state = await new ChatInterviewAdapter(deps.appData, deps.events).get(interviewSessionId);
|
|
3502
3591
|
if (!state || state.interview.operatorSessionId !== sessionId) {
|
|
@@ -3594,7 +3683,7 @@ async function handleContractApplyHumanGate(req, res, deps, sessionId, step) {
|
|
|
3594
3683
|
});
|
|
3595
3684
|
return;
|
|
3596
3685
|
}
|
|
3597
|
-
if (!(await
|
|
3686
|
+
if (!(await requireWorkflowSession(res, deps, sessionId)))
|
|
3598
3687
|
return;
|
|
3599
3688
|
const ctx = deps.runtime.actionContext;
|
|
3600
3689
|
if (!ctx) {
|
|
@@ -3776,7 +3865,7 @@ async function handleDagHumanGate(req, res, deps, sessionId, step) {
|
|
|
3776
3865
|
});
|
|
3777
3866
|
return;
|
|
3778
3867
|
}
|
|
3779
|
-
if (!(await
|
|
3868
|
+
if (!(await requireWorkflowSession(res, deps, sessionId)))
|
|
3780
3869
|
return;
|
|
3781
3870
|
const ctx = deps.runtime.actionContext;
|
|
3782
3871
|
if (!ctx) {
|
|
@@ -3885,7 +3974,7 @@ async function handleMutationHumanGate(req, res, deps, sessionId) {
|
|
|
3885
3974
|
});
|
|
3886
3975
|
return;
|
|
3887
3976
|
}
|
|
3888
|
-
if (!(await
|
|
3977
|
+
if (!(await requireWorkflowSession(res, deps, sessionId)))
|
|
3889
3978
|
return;
|
|
3890
3979
|
const ctx = deps.runtime.actionContext;
|
|
3891
3980
|
if (!ctx) {
|
|
@@ -3974,7 +4063,7 @@ async function handleConfirmTaskKind(req, res, deps, sessionId, interviewSession
|
|
|
3974
4063
|
});
|
|
3975
4064
|
return;
|
|
3976
4065
|
}
|
|
3977
|
-
if (!(await
|
|
4066
|
+
if (!(await requireWorkflowSession(res, deps, sessionId)))
|
|
3978
4067
|
return;
|
|
3979
4068
|
try {
|
|
3980
4069
|
const body = await readJsonBody(req);
|
|
@@ -4030,13 +4119,7 @@ export async function handleChatCompact(req, res, deps, sessionId) {
|
|
|
4030
4119
|
return;
|
|
4031
4120
|
}
|
|
4032
4121
|
if (!deps.runtime.hasSession(sessionId)) {
|
|
4033
|
-
const reopened = await deps.runtime.reopenSession(
|
|
4034
|
-
sessionId,
|
|
4035
|
-
sessionFile: record.sessionFile,
|
|
4036
|
-
...(record.modelProvider && record.modelId
|
|
4037
|
-
? { model: { provider: record.modelProvider, modelId: record.modelId } }
|
|
4038
|
-
: {}),
|
|
4039
|
-
});
|
|
4122
|
+
const reopened = await deps.runtime.reopenSession(reopenInitFromRecord(record));
|
|
4040
4123
|
if (!reopened.ok) {
|
|
4041
4124
|
const mapped = await mapFailedReopen(deps, sessionId, reopened);
|
|
4042
4125
|
sendJson(res, mapped.status, {
|
|
@@ -4060,7 +4143,9 @@ export async function handleChatCompact(req, res, deps, sessionId) {
|
|
|
4060
4143
|
const rawInstructions = typeof body.instructions === "string" ? body.instructions : undefined;
|
|
4061
4144
|
const currentTodos = projectCurrentTodos(deps.events.snapshot(sessionId));
|
|
4062
4145
|
const todoInstructions = buildTodoCompactionInstructions(rawInstructions, currentTodos);
|
|
4063
|
-
const customInstructions =
|
|
4146
|
+
const customInstructions = isWorkflowSessionMode(record.sessionMode)
|
|
4147
|
+
? buildGoalCompactionInstructions(todoInstructions, deps.goalRegistry?.get(sessionId) ?? null)
|
|
4148
|
+
: todoInstructions;
|
|
4064
4149
|
const turnId = deps.events.latestTurnId(sessionId) ?? `${sessionId}:compact`;
|
|
4065
4150
|
const compactionId = `${sessionId}:compaction:${randomUUID()}`;
|
|
4066
4151
|
deps.events.append(sessionId, turnId, {
|
|
@@ -4146,6 +4231,42 @@ export async function handleChatMessages(res, deps, sessionId, url) {
|
|
|
4146
4231
|
export async function handleChatRequest(req, res, deps, pathname, workspaceScope) {
|
|
4147
4232
|
if (!deps)
|
|
4148
4233
|
return false;
|
|
4234
|
+
try {
|
|
4235
|
+
return await dispatchChatRequest(req, res, deps, pathname, workspaceScope);
|
|
4236
|
+
}
|
|
4237
|
+
catch (error) {
|
|
4238
|
+
// Store-level fail-closed for corrupt sessionMode: bounded 409 instead of
|
|
4239
|
+
// a generic 500, for any handler that reads the record without its own
|
|
4240
|
+
// UnknownChatSessionModeError mapping (readChatSessionMode covers the rest).
|
|
4241
|
+
if (error instanceof UnknownChatSessionModeError) {
|
|
4242
|
+
sendJson(res, 409, {
|
|
4243
|
+
ok: false,
|
|
4244
|
+
error: { code: error.code, message: error.message },
|
|
4245
|
+
});
|
|
4246
|
+
return true;
|
|
4247
|
+
}
|
|
4248
|
+
throw error;
|
|
4249
|
+
}
|
|
4250
|
+
}
|
|
4251
|
+
async function dispatchChatRequest(req, res, deps, pathname, workspaceScope) {
|
|
4252
|
+
// Terminal management API (model + Console UI shared PTY registry).
|
|
4253
|
+
if (await handleTerminalRequest(req, res, {
|
|
4254
|
+
terminals: deps.terminals ?? new TerminalSessionRegistry(),
|
|
4255
|
+
gateMutation: (r) => gateMutation(r, deps),
|
|
4256
|
+
// Terminals open in the workspace repo root, not the Console process cwd.
|
|
4257
|
+
defaultCwd: deps.appData.repoRoot,
|
|
4258
|
+
}, pathname)) {
|
|
4259
|
+
return true;
|
|
4260
|
+
}
|
|
4261
|
+
// Source control API (model + Console UI shared workspace ScmService).
|
|
4262
|
+
if (deps.scm) {
|
|
4263
|
+
if (await handleScmRequest(req, res, {
|
|
4264
|
+
scm: deps.scm,
|
|
4265
|
+
gateMutation: (r) => gateMutation(r, deps),
|
|
4266
|
+
}, pathname)) {
|
|
4267
|
+
return true;
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4149
4270
|
if (!pathname.startsWith("/api/operator/v1/chat/") &&
|
|
4150
4271
|
!pathname.startsWith("/api/operator/v1/pi/"))
|
|
4151
4272
|
return false;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operator Chat — source control HTTP API (Console UI side).
|
|
3
|
+
*
|
|
4
|
+
* Endpoints (all under /api/operator/v1/chat):
|
|
5
|
+
* GET /scm/status branch + changes + counts
|
|
6
|
+
* GET /scm/diff?path=&staged= unified diff (one path or --stat)
|
|
7
|
+
* POST /scm/stage { paths: string[] } | { all: true }
|
|
8
|
+
* POST /scm/unstage { paths: string[] }
|
|
9
|
+
* POST /scm/discard { paths: string[] } [dangerous]
|
|
10
|
+
* POST /scm/commit { message }
|
|
11
|
+
* GET /scm/log?limit= recent commits
|
|
12
|
+
* GET /scm/branches branch list + current
|
|
13
|
+
* POST /scm/branches { name, create? } switch or create
|
|
14
|
+
* POST /scm/push push to upstream
|
|
15
|
+
* POST /scm/pull pull from upstream
|
|
16
|
+
*
|
|
17
|
+
* Reads are open (same-origin console); every mutation passes gateMutation.
|
|
18
|
+
* The ScmService is workspace-scoped (repoRoot fixed at construction).
|
|
19
|
+
*/
|
|
20
|
+
function sendJson(res, status, body) {
|
|
21
|
+
res.writeHead(status, {
|
|
22
|
+
"content-type": "application/json; charset=utf-8",
|
|
23
|
+
"cache-control": "no-store",
|
|
24
|
+
});
|
|
25
|
+
res.end(JSON.stringify(body));
|
|
26
|
+
}
|
|
27
|
+
function mapScmError(res, error) {
|
|
28
|
+
const status = error.code === "NO_PATHS" || error.code === "EMPTY_MESSAGE" || error.code === "INVALID_NAME"
|
|
29
|
+
? 400
|
|
30
|
+
: error.code === "GIT_TIMEOUT"
|
|
31
|
+
? 504
|
|
32
|
+
: 500;
|
|
33
|
+
sendJson(res, status, {
|
|
34
|
+
ok: false,
|
|
35
|
+
error: { code: error.code, message: error.message, ...(error.stderr ? { stderr: error.stderr } : {}) },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async function readJsonBody(req, maxBytes = 256 * 1024) {
|
|
39
|
+
const chunks = [];
|
|
40
|
+
let total = 0;
|
|
41
|
+
for await (const chunk of req) {
|
|
42
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
43
|
+
total += buf.length;
|
|
44
|
+
if (total > maxBytes)
|
|
45
|
+
throw new Error("request body too large");
|
|
46
|
+
chunks.push(buf);
|
|
47
|
+
}
|
|
48
|
+
if (chunks.length === 0)
|
|
49
|
+
return {};
|
|
50
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
51
|
+
if (!raw.trim())
|
|
52
|
+
return {};
|
|
53
|
+
return JSON.parse(raw);
|
|
54
|
+
}
|
|
55
|
+
function parsePaths(body) {
|
|
56
|
+
if (!Array.isArray(body.paths))
|
|
57
|
+
return [];
|
|
58
|
+
return body.paths
|
|
59
|
+
.filter((p) => typeof p === "string" && p.length > 0 && !p.includes("\0"))
|
|
60
|
+
.slice(0, 500);
|
|
61
|
+
}
|
|
62
|
+
/** Route dispatcher; returns true when the request was handled. */
|
|
63
|
+
export async function handleScmRequest(req, res, deps, pathname) {
|
|
64
|
+
if (!pathname.startsWith("/api/operator/v1/chat/scm"))
|
|
65
|
+
return false;
|
|
66
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
67
|
+
const rest = pathname.slice("/api/operator/v1/chat/scm".length);
|
|
68
|
+
const parts = rest.split("/").filter(Boolean);
|
|
69
|
+
const sub = parts[0] ?? "";
|
|
70
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
71
|
+
const gateFailed = (gate) => {
|
|
72
|
+
sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
|
|
73
|
+
return true;
|
|
74
|
+
};
|
|
75
|
+
try {
|
|
76
|
+
if (method === "GET" && sub === "status" && parts.length === 1) {
|
|
77
|
+
const r = await deps.scm.status();
|
|
78
|
+
if (!r.ok)
|
|
79
|
+
return mapScmError(res, r), true;
|
|
80
|
+
sendJson(res, 200, { ok: true, data: r });
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
if (method === "GET" && sub === "diff" && parts.length === 1) {
|
|
84
|
+
const path = url.searchParams.get("path") ?? undefined;
|
|
85
|
+
const staged = url.searchParams.get("staged") === "1";
|
|
86
|
+
const r = await deps.scm.diff({ path, staged });
|
|
87
|
+
if (!r.ok)
|
|
88
|
+
return mapScmError(res, r), true;
|
|
89
|
+
sendJson(res, 200, { ok: true, data: r });
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if (method === "GET" && sub === "log" && parts.length === 1) {
|
|
93
|
+
const limit = Number(url.searchParams.get("limit") ?? 30);
|
|
94
|
+
const r = await deps.scm.log(Number.isFinite(limit) ? Math.min(Math.max(limit, 1), 200) : 30);
|
|
95
|
+
if (!r.ok)
|
|
96
|
+
return mapScmError(res, r), true;
|
|
97
|
+
sendJson(res, 200, { ok: true, data: r });
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
if (method === "GET" && sub === "branches" && parts.length === 1) {
|
|
101
|
+
const r = await deps.scm.branchList();
|
|
102
|
+
if (!r.ok)
|
|
103
|
+
return mapScmError(res, r), true;
|
|
104
|
+
sendJson(res, 200, { ok: true, data: r });
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
// Everything below mutates → gate first.
|
|
108
|
+
const gate = deps.gateMutation(req);
|
|
109
|
+
if (!gate.ok)
|
|
110
|
+
return gateFailed(gate);
|
|
111
|
+
if (method === "POST" && sub === "stage" && parts.length === 1) {
|
|
112
|
+
const body = await readJsonBody(req);
|
|
113
|
+
const r = body.all === true
|
|
114
|
+
? await deps.scm.stage([], true)
|
|
115
|
+
: await deps.scm.stage(parsePaths(body));
|
|
116
|
+
if (!r.ok)
|
|
117
|
+
return mapScmError(res, r), true;
|
|
118
|
+
sendJson(res, 200, { ok: true });
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
if (method === "POST" && sub === "unstage" && parts.length === 1) {
|
|
122
|
+
const body = await readJsonBody(req);
|
|
123
|
+
const r = await deps.scm.unstage(parsePaths(body));
|
|
124
|
+
if (!r.ok)
|
|
125
|
+
return mapScmError(res, r), true;
|
|
126
|
+
sendJson(res, 200, { ok: true });
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
if (method === "POST" && sub === "discard" && parts.length === 1) {
|
|
130
|
+
const body = await readJsonBody(req);
|
|
131
|
+
const r = await deps.scm.discard(parsePaths(body));
|
|
132
|
+
if (!r.ok)
|
|
133
|
+
return mapScmError(res, r), true;
|
|
134
|
+
sendJson(res, 200, { ok: true });
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
if (method === "POST" && sub === "commit" && parts.length === 1) {
|
|
138
|
+
const body = await readJsonBody(req);
|
|
139
|
+
const message = typeof body.message === "string" ? body.message : "";
|
|
140
|
+
const r = await deps.scm.commit(message);
|
|
141
|
+
if (!r.ok)
|
|
142
|
+
return mapScmError(res, r), true;
|
|
143
|
+
sendJson(res, 200, { ok: true, data: r });
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
if (method === "POST" && sub === "branches" && parts.length === 1) {
|
|
147
|
+
const body = await readJsonBody(req);
|
|
148
|
+
const name = typeof body.name === "string" ? body.name : "";
|
|
149
|
+
if (!name) {
|
|
150
|
+
sendJson(res, 400, { ok: false, error: { code: "INVALID_INPUT", message: "name is required" } });
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
const r = body.create === true
|
|
154
|
+
? await deps.scm.branchCreate(name)
|
|
155
|
+
: await deps.scm.branchSwitch(name);
|
|
156
|
+
if (!r.ok)
|
|
157
|
+
return mapScmError(res, r), true;
|
|
158
|
+
sendJson(res, 200, { ok: true });
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
if (method === "POST" && sub === "push" && parts.length === 1) {
|
|
162
|
+
const r = await deps.scm.push();
|
|
163
|
+
if (!r.ok)
|
|
164
|
+
return mapScmError(res, r), true;
|
|
165
|
+
sendJson(res, 200, { ok: true, data: r });
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
if (method === "POST" && sub === "pull" && parts.length === 1) {
|
|
169
|
+
const r = await deps.scm.pull();
|
|
170
|
+
if (!r.ok)
|
|
171
|
+
return mapScmError(res, r), true;
|
|
172
|
+
sendJson(res, 200, { ok: true, data: r });
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
sendJson(res, 404, { ok: false, error: { code: "NOT_FOUND", message: `unknown scm route: ${method} ${pathname}` } });
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
if (!res.headersSent) {
|
|
180
|
+
sendJson(res, 500, {
|
|
181
|
+
ok: false,
|
|
182
|
+
error: {
|
|
183
|
+
code: "SCM_INTERNAL",
|
|
184
|
+
message: error instanceof Error ? error.message : String(error),
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
res.end();
|
|
190
|
+
}
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
}
|