@cloudcli-ai/cloudcli 1.37.0 → 1.37.2
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/dist/assets/abnfDiagram-N423BO3Z-C4UAoHJb.js +1 -0
- package/dist/assets/arc-B8jF-ja9.js +1 -0
- package/dist/assets/architectureDiagram-T3A2C74G-C_Wur_CU.js +36 -0
- package/dist/assets/blockDiagram-VBNYF7ZC-Cq4P1i8t.js +132 -0
- package/dist/assets/c4Diagram-5PPSVZJV-C1wG4eUj.js +10 -0
- package/dist/assets/channel-_falYbU1.js +1 -0
- package/dist/assets/chunk-2GRJ4B5K-B8fnBG6E.js +1 -0
- package/dist/assets/chunk-2Q5K7J3B-WdfPooPJ.js +1 -0
- package/dist/assets/chunk-5RXB4S5H-DhZJuG2E.js +231 -0
- package/dist/assets/chunk-5VM5RSS4-B0rjslal.js +15 -0
- package/dist/assets/chunk-6Q2QTUOP-CG2Oj4jL.js +88 -0
- package/dist/assets/chunk-GF5L2VYU-rrl2lRxY.js +206 -0
- package/dist/assets/chunk-JWPE2WC7-CjVbrVMA.js +1 -0
- package/dist/assets/chunk-KBJHAD2P-CRzKgU8S.js +1 -0
- package/dist/assets/chunk-RYQCIY6F-DBz4GTZF.js +1 -0
- package/dist/assets/chunk-XXDRQBXY-wHxalsXk.js +1 -0
- package/dist/assets/classDiagram-JCYQIIEL-Dn-3CfpR.js +1 -0
- package/dist/assets/classDiagram-v2-OCEON4UE-Dn-3CfpR.js +1 -0
- package/dist/assets/cose-bilkent-JH36ORCC-s1hygY8e.js +1 -0
- package/dist/assets/cynefin-VYW2F7L2-C4LGV15i.js +178 -0
- package/dist/assets/cynefinDiagram-MW4NZA55-C5uhu7B3.js +62 -0
- package/dist/assets/cytoscape.esm-C10L9Icf.js +331 -0
- package/dist/assets/dagre-VZM6K2ZE-B-OBTewm.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-7IWD3JNH-BjGKQxih.js +30 -0
- package/dist/assets/diagram-B4RE2ZJO-BMKz1X2L.js +3 -0
- package/dist/assets/diagram-LBJQPF4R-DNh-GhKC.js +24 -0
- package/dist/assets/diagram-Q27KOJAE-BaTG9jPT.js +24 -0
- package/dist/assets/diagram-UB23O5K3-BUwo2OvJ.js +41 -0
- package/dist/assets/ebnfDiagram-BXEA7PRR-DAZgfzX2.js +1 -0
- package/dist/assets/erDiagram-JOGREHBK-1w1CcGEb.js +85 -0
- package/dist/assets/flowDiagram-UKHOOZJN-D4EII8Jj.js +156 -0
- package/dist/assets/ganttDiagram-PKOTCBZU-ChvJZAgB.js +292 -0
- package/dist/assets/gitGraphDiagram-DS77QQ5N-DtOn41S8.js +106 -0
- package/dist/assets/graph-Cx-EqisW.js +1 -0
- package/dist/assets/index-CzPLBvXT.css +32 -0
- package/dist/assets/index-DFkBBuSA.js +1549 -0
- package/dist/assets/infoDiagram-6WML65LV-DN_PVEJZ.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/ishikawaDiagram-WSZJBQD7-DbkF1kXb.js +70 -0
- package/dist/assets/journeyDiagram-NVQOT4AX-pJwmhddE.js +139 -0
- package/dist/assets/kanban-definition-27J2QSJJ-PPAPgNp5.js +89 -0
- package/dist/assets/layout-D6BstSzF.js +1 -0
- package/dist/assets/linear-iPR-J9f7.js +1 -0
- package/dist/assets/map-CV9uif0g.js +1 -0
- package/dist/assets/mermaid.core-BEUL86Z6.js +312 -0
- package/dist/assets/mindmap-definition-FAOFIHXS-BvZoBib7.js +96 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pegDiagram-VL7TDLO6-DhYreaUd.js +1 -0
- package/dist/assets/pieDiagram-7S7Q4E2Y-CrOHlyD0.js +39 -0
- package/dist/assets/quadrantDiagram-CIZ2JOQS-D4g5yH3t.js +7 -0
- package/dist/assets/railroadDiagram-AXF67PYL-Dj39lHlf.js +1 -0
- package/dist/assets/requirementDiagram-LRYGKXZP-1AS4Zgm_.js +84 -0
- package/dist/assets/sankeyDiagram-W5VNT64P-CKlMn19P.js +40 -0
- package/dist/assets/sequenceDiagram-SI44F4Z6-B_j3qyvo.js +162 -0
- package/dist/assets/sizeCapture-X5ZJPWSS-Bp2Op16v.js +1 -0
- package/dist/assets/stateDiagram-OKZ733FA-C0cN6DoY.js +1 -0
- package/dist/assets/stateDiagram-v2-UEYNNEHI-BvBxUUrZ.js +1 -0
- package/dist/assets/swimlanes-SLNWSIFB-BEvXOV9b.js +2 -0
- package/dist/assets/swimlanesDiagram-ULZ7WXOC-CsE-x4YK.js +8 -0
- package/dist/assets/timeline-definition-Z64GVDOM-zdJQinvZ.js +120 -0
- package/dist/assets/vennDiagram-T6HMQDX7-DuF1uZMY.js +34 -0
- package/dist/assets/wardleyDiagram-T6FBY63Y-B9AlLQup.js +78 -0
- package/dist/assets/xychartDiagram-ELKLHX3M-DTJX48Qg.js +7 -0
- package/dist/index.html +2 -2
- package/dist-server/server/index.js +1 -2
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/agent/agent.module.js +2 -1
- package/dist-server/server/modules/agent/agent.module.js.map +1 -1
- package/dist-server/server/modules/auth/auth.middleware.js +1 -1
- package/dist-server/server/modules/auth/auth.middleware.js.map +1 -1
- package/dist-server/server/modules/browser-use/browser-use-runtime.js +10 -0
- package/dist-server/server/modules/browser-use/browser-use-runtime.js.map +1 -0
- package/dist-server/server/modules/browser-use/browser-use.service.js +3 -6
- package/dist-server/server/modules/browser-use/browser-use.service.js.map +1 -1
- package/dist-server/server/modules/browser-use/index.js +2 -0
- package/dist-server/server/modules/browser-use/index.js.map +1 -1
- package/dist-server/server/modules/cli/cli.js +9 -3
- package/dist-server/server/modules/cli/cli.js.map +1 -1
- package/dist-server/server/modules/cli/cli.module.js +0 -1
- package/dist-server/server/modules/cli/cli.module.js.map +1 -1
- package/dist-server/server/modules/cli/cli.service.js +0 -18
- package/dist-server/server/modules/cli/cli.service.js.map +1 -1
- package/dist-server/server/modules/cli/tests/cli-environment-bootstrap.test.js +154 -0
- package/dist-server/server/modules/cli/tests/cli-environment-bootstrap.test.js.map +1 -0
- package/dist-server/server/modules/cli/tests/cli.service.test.js +0 -3
- package/dist-server/server/modules/cli/tests/cli.service.test.js.map +1 -1
- package/dist-server/server/modules/commands/commands.routes.js +3 -3
- package/dist-server/server/modules/commands/commands.routes.js.map +1 -1
- package/dist-server/server/modules/commands/tests/commands.test.js +2 -7
- package/dist-server/server/modules/commands/tests/commands.test.js.map +1 -1
- package/dist-server/server/modules/database/index.js +2 -0
- package/dist-server/server/modules/database/index.js.map +1 -1
- package/dist-server/server/modules/database/migrations.js +18 -1
- package/dist-server/server/modules/database/migrations.js.map +1 -1
- package/dist-server/server/modules/database/repositories/provider-models.js +112 -0
- package/dist-server/server/modules/database/repositories/provider-models.js.map +1 -0
- package/dist-server/server/modules/database/repositories/sessions.db.js +63 -7
- package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
- package/dist-server/server/modules/database/schema.js +28 -3
- package/dist-server/server/modules/database/schema.js.map +1 -1
- package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js +102 -0
- package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js.map +1 -0
- package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js +2 -1
- package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js.map +1 -1
- package/dist-server/server/modules/database/tests/sessions.db.integration.test.js +22 -0
- package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -1
- package/dist-server/server/modules/file-tree/file-tree.module.js +5 -1
- package/dist-server/server/modules/file-tree/file-tree.module.js.map +1 -1
- package/dist-server/server/modules/file-tree/file-tree.service.js +60 -20
- package/dist-server/server/modules/file-tree/file-tree.service.js.map +1 -1
- package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js +143 -16
- package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js.map +1 -1
- package/dist-server/server/modules/git/git-branch.service.js +15 -0
- package/dist-server/server/modules/git/git-branch.service.js.map +1 -0
- package/dist-server/server/modules/git/git.routes.js +15 -7
- package/dist-server/server/modules/git/git.routes.js.map +1 -1
- package/dist-server/server/modules/git/tests/git-branch.service.test.js +52 -0
- package/dist-server/server/modules/git/tests/git-branch.service.test.js.map +1 -0
- package/dist-server/server/modules/git/tests/git-init.routes.test.js +84 -0
- package/dist-server/server/modules/git/tests/git-init.routes.test.js.map +1 -1
- package/dist-server/server/modules/notifications/index.js +3 -1
- package/dist-server/server/modules/notifications/index.js.map +1 -1
- package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +22 -1
- package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +46 -14
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +259 -60
- package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +58 -71
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +1 -54
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +7 -7
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +32 -669
- package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +105 -328
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +0 -12
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +144 -31
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-models.service.js +167 -199
- package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-runtime.service.js +2 -2
- package/dist-server/server/modules/providers/services/provider-runtime.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js +75 -11
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions.service.js +62 -3
- package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js +53 -17
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-models.test.js +15 -126
- package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +5 -5
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js +175 -292
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider-runtime.service.test.js +2 -6
- package/dist-server/server/modules/providers/tests/provider-runtime.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js +1 -0
- package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider.routes.test.js +161 -0
- package/dist-server/server/modules/providers/tests/provider.routes.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/sessions.service.test.js +87 -0
- package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -0
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +5 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
- package/dist-server/server/shared/utils.js +8 -0
- package/dist-server/server/shared/utils.js.map +1 -1
- package/package.json +8 -4
- package/scripts/promote-dist-server.mjs +50 -0
- package/server/index.ts +1 -2
- package/server/modules/agent/agent.module.ts +2 -1
- package/server/modules/auth/auth.middleware.ts +2 -2
- package/server/modules/browser-use/browser-use-runtime.ts +10 -0
- package/server/modules/browser-use/browser-use.service.ts +5 -8
- package/server/modules/browser-use/index.ts +3 -0
- package/server/modules/cli/cli.module.ts +0 -1
- package/server/modules/cli/cli.service.ts +0 -20
- package/server/modules/cli/cli.ts +9 -3
- package/server/modules/cli/tests/cli-environment-bootstrap.test.ts +186 -0
- package/server/modules/cli/tests/cli.service.test.ts +0 -3
- package/server/modules/commands/commands.routes.ts +3 -3
- package/server/modules/commands/tests/commands.test.ts +2 -7
- package/server/modules/database/index.ts +2 -0
- package/server/modules/database/migrations.ts +20 -0
- package/server/modules/database/repositories/provider-models.ts +168 -0
- package/server/modules/database/repositories/sessions.db.ts +85 -8
- package/server/modules/database/schema.ts +29 -3
- package/server/modules/database/tests/provider-models.db.integration.test.ts +125 -0
- package/server/modules/database/tests/sessions-provider-mapping.test.ts +2 -1
- package/server/modules/database/tests/sessions.db.integration.test.ts +32 -0
- package/server/modules/file-tree/file-tree.module.ts +5 -1
- package/server/modules/file-tree/file-tree.service.ts +81 -22
- package/server/modules/file-tree/tests/file-tree.service.test.ts +167 -16
- package/server/modules/git/git-branch.service.ts +43 -0
- package/server/modules/git/git.routes.ts +16 -9
- package/server/modules/git/tests/git-branch.service.test.ts +60 -0
- package/server/modules/git/tests/git-init.routes.test.ts +83 -0
- package/server/modules/notifications/index.ts +2 -0
- package/server/modules/notifications/services/notification-orchestrator.service.js +24 -1
- package/server/modules/providers/list/claude/claude-models.provider.ts +46 -14
- package/server/modules/providers/list/claude/claude-runtime.provider.js +280 -67
- package/server/modules/providers/list/codex/codex-models.provider.ts +58 -99
- package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +1 -60
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +7 -7
- package/server/modules/providers/list/cursor/cursor-models.provider.ts +32 -710
- package/server/modules/providers/list/opencode/opencode-models.provider.ts +105 -413
- package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +0 -13
- package/server/modules/providers/provider.routes.ts +190 -33
- package/server/modules/providers/services/provider-models.service.ts +223 -288
- package/server/modules/providers/services/provider-runtime.service.ts +2 -3
- package/server/modules/providers/services/session-conversations-search.service.ts +106 -11
- package/server/modules/providers/services/sessions.service.ts +87 -3
- package/server/modules/providers/tests/codex-sessions.test.ts +57 -18
- package/server/modules/providers/tests/opencode-models.test.ts +34 -135
- package/server/modules/providers/tests/opencode-sessions.test.ts +5 -5
- package/server/modules/providers/tests/provider-models.service.test.ts +353 -450
- package/server/modules/providers/tests/provider-runtime.service.test.ts +2 -6
- package/server/modules/providers/tests/provider-token-usage.service.test.ts +1 -0
- package/server/modules/providers/tests/provider.routes.test.ts +244 -0
- package/server/modules/providers/tests/sessions.service.test.ts +127 -0
- package/server/modules/websocket/services/chat-websocket.service.ts +5 -1
- package/server/shared/interfaces.ts +4 -6
- package/server/shared/types.ts +29 -18
- package/server/shared/utils.ts +10 -0
- package/server/tsconfig.json +6 -2
- package/README.de.md +0 -257
- package/README.ja.md +0 -249
- package/README.ko.md +0 -249
- package/README.md +0 -269
- package/README.ru.md +0 -257
- package/README.tr.md +0 -258
- package/README.zh-CN.md +0 -249
- package/README.zh-TW.md +0 -249
- package/dist/assets/index-DSlds633.css +0 -32
- package/dist/assets/index-eGqPeIEu.js +0 -1531
- package/dist-server/server/constants/config.js +0 -6
- package/dist-server/server/constants/config.js.map +0 -1
- package/server/constants/config.ts +0 -5
|
@@ -11,13 +11,20 @@ type SessionRow = {
|
|
|
11
11
|
custom_name: string | null;
|
|
12
12
|
/** Model this session runs with; NULL until the app records one for it. */
|
|
13
13
|
model: string | null;
|
|
14
|
+
/** Reasoning effort this session runs with; NULL until the app records one. */
|
|
15
|
+
effort: string | null;
|
|
14
16
|
isArchived: number;
|
|
15
17
|
created_at: string;
|
|
16
18
|
updated_at: string;
|
|
17
19
|
};
|
|
18
20
|
|
|
21
|
+
type RecentSessionsPage = {
|
|
22
|
+
sessions: SessionRow[];
|
|
23
|
+
total: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
19
26
|
const SESSION_ROW_COLUMNS =
|
|
20
|
-
'session_id, provider, provider_session_id, project_path, jsonl_path, custom_name, model, isArchived, created_at, updated_at';
|
|
27
|
+
'session_id, provider, provider_session_id, project_path, jsonl_path, custom_name, model, effort, isArchived, created_at, updated_at';
|
|
21
28
|
|
|
22
29
|
const SQLITE_UTC_TIMESTAMP_REGEX = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
|
|
23
30
|
|
|
@@ -67,7 +74,9 @@ export const sessionsDb = {
|
|
|
67
74
|
* The given id is the provider-native session id. Rows are keyed by
|
|
68
75
|
* `provider_session_id` so a session that was first created by the app
|
|
69
76
|
* (with an app-allocated `session_id`) is updated in place once its
|
|
70
|
-
* transcript shows up on disk, instead of producing a duplicate row.
|
|
77
|
+
* transcript shows up on disk, instead of producing a duplicate row. An
|
|
78
|
+
* app-created row keeps its existing name; synchronizer names only update
|
|
79
|
+
* rows that were themselves created by indexing provider storage.
|
|
71
80
|
*/
|
|
72
81
|
createSession(
|
|
73
82
|
providerSessionId: string,
|
|
@@ -103,7 +112,10 @@ export const sessionsDb = {
|
|
|
103
112
|
project_path = ?,
|
|
104
113
|
jsonl_path = ?,
|
|
105
114
|
isArchived = 0,
|
|
106
|
-
custom_name =
|
|
115
|
+
custom_name = CASE
|
|
116
|
+
WHEN session_id <> provider_session_id AND custom_name IS NOT NULL THEN custom_name
|
|
117
|
+
ELSE COALESCE(?, custom_name)
|
|
118
|
+
END
|
|
107
119
|
WHERE session_id = ?`
|
|
108
120
|
).run(
|
|
109
121
|
provider,
|
|
@@ -130,7 +142,11 @@ export const sessionsDb = {
|
|
|
130
142
|
project_path = excluded.project_path,
|
|
131
143
|
jsonl_path = excluded.jsonl_path,
|
|
132
144
|
isArchived = 0,
|
|
133
|
-
custom_name =
|
|
145
|
+
custom_name = CASE
|
|
146
|
+
WHEN sessions.session_id <> sessions.provider_session_id AND sessions.custom_name IS NOT NULL
|
|
147
|
+
THEN sessions.custom_name
|
|
148
|
+
ELSE COALESCE(excluded.custom_name, sessions.custom_name)
|
|
149
|
+
END`
|
|
134
150
|
).run(
|
|
135
151
|
providerSessionId,
|
|
136
152
|
provider,
|
|
@@ -151,9 +167,15 @@ export const sessionsDb = {
|
|
|
151
167
|
* The session gateway uses this when the frontend starts a brand-new chat:
|
|
152
168
|
* `session_id` is the stable app-facing id, while `provider_session_id`
|
|
153
169
|
* stays NULL until the provider runtime announces its own id and
|
|
154
|
-
* `assignProviderSessionId` records the mapping.
|
|
170
|
+
* `assignProviderSessionId` records the mapping. `customName` is derived
|
|
171
|
+
* from the first visible CloudCLI message by the sessions service.
|
|
155
172
|
*/
|
|
156
|
-
createAppSession(
|
|
173
|
+
createAppSession(
|
|
174
|
+
sessionId: string,
|
|
175
|
+
provider: string,
|
|
176
|
+
projectPath: string,
|
|
177
|
+
customName?: string,
|
|
178
|
+
): string {
|
|
157
179
|
const db = getConnection();
|
|
158
180
|
const normalizedProjectPath = normalizeProjectPathForProvider(provider, projectPath);
|
|
159
181
|
|
|
@@ -161,8 +183,8 @@ export const sessionsDb = {
|
|
|
161
183
|
|
|
162
184
|
db.prepare(
|
|
163
185
|
`INSERT INTO sessions (session_id, provider, provider_session_id, custom_name, project_path, jsonl_path, isArchived, created_at, updated_at)
|
|
164
|
-
VALUES (?, ?, NULL,
|
|
165
|
-
).run(sessionId, provider, normalizedProjectPath);
|
|
186
|
+
VALUES (?, ?, NULL, ?, ?, NULL, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`
|
|
187
|
+
).run(sessionId, provider, customName ?? null, normalizedProjectPath);
|
|
166
188
|
|
|
167
189
|
return sessionId;
|
|
168
190
|
},
|
|
@@ -229,6 +251,21 @@ export const sessionsDb = {
|
|
|
229
251
|
).run(model, sessionId);
|
|
230
252
|
},
|
|
231
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Records the reasoning effort one session runs with.
|
|
256
|
+
*
|
|
257
|
+
* `default` is stored as an explicit choice rather than NULL so reopening
|
|
258
|
+
* the session does not inherit a later per-provider effort preference.
|
|
259
|
+
*/
|
|
260
|
+
setSessionEffort(sessionId: string, effort: string): void {
|
|
261
|
+
const db = getConnection();
|
|
262
|
+
db.prepare(
|
|
263
|
+
`UPDATE sessions
|
|
264
|
+
SET effort = ?
|
|
265
|
+
WHERE session_id = ?`
|
|
266
|
+
).run(effort, sessionId);
|
|
267
|
+
},
|
|
268
|
+
|
|
232
269
|
updateSessionCustomName(sessionId: string, customName: string): void {
|
|
233
270
|
const db = getConnection();
|
|
234
271
|
db.prepare(
|
|
@@ -325,6 +362,46 @@ export const sessionsDb = {
|
|
|
325
362
|
return normalizeSessionRows(rows);
|
|
326
363
|
},
|
|
327
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Returns one globally ordered page of visible conversations.
|
|
367
|
+
*
|
|
368
|
+
* Pagination happens after archived sessions and sessions belonging to an
|
|
369
|
+
* archived project have been excluded. This keeps the sidebar feed complete
|
|
370
|
+
* and correctly ordered across projects instead of flattening only the
|
|
371
|
+
* per-project slices already loaded by the client.
|
|
372
|
+
*/
|
|
373
|
+
getRecentSessionsPage(limit: number, offset: number): RecentSessionsPage {
|
|
374
|
+
const db = getConnection();
|
|
375
|
+
const visibilityClause = `
|
|
376
|
+
sessions.isArchived = 0
|
|
377
|
+
AND (projects.isArchived IS NULL OR projects.isArchived = 0)
|
|
378
|
+
`;
|
|
379
|
+
const rows = db
|
|
380
|
+
.prepare(
|
|
381
|
+
`SELECT sessions.*
|
|
382
|
+
FROM sessions
|
|
383
|
+
LEFT JOIN projects ON projects.project_path = sessions.project_path
|
|
384
|
+
WHERE ${visibilityClause}
|
|
385
|
+
ORDER BY julianday(COALESCE(sessions.updated_at, sessions.created_at)) DESC,
|
|
386
|
+
sessions.session_id DESC
|
|
387
|
+
LIMIT ? OFFSET ?`
|
|
388
|
+
)
|
|
389
|
+
.all(limit, offset) as SessionRow[];
|
|
390
|
+
const countRow = db
|
|
391
|
+
.prepare(
|
|
392
|
+
`SELECT COUNT(*) AS count
|
|
393
|
+
FROM sessions
|
|
394
|
+
LEFT JOIN projects ON projects.project_path = sessions.project_path
|
|
395
|
+
WHERE ${visibilityClause}`
|
|
396
|
+
)
|
|
397
|
+
.get() as { count: number } | undefined;
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
sessions: normalizeSessionRows(rows),
|
|
401
|
+
total: Number(countRow?.count ?? 0),
|
|
402
|
+
};
|
|
403
|
+
},
|
|
404
|
+
|
|
328
405
|
/**
|
|
329
406
|
* Archived rows are intentionally queried separately so the caller can render
|
|
330
407
|
* them in a dedicated view without reintroducing them into active session lists.
|
|
@@ -109,10 +109,11 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
109
109
|
custom_name TEXT,
|
|
110
110
|
project_path TEXT,
|
|
111
111
|
jsonl_path TEXT,
|
|
112
|
-
-- Model this session runs with. Written when the user
|
|
113
|
-
--
|
|
114
|
-
--
|
|
112
|
+
-- Model and reasoning effort this session runs with. Written when the user
|
|
113
|
+
-- changes either selection and on every send, so reopening a session
|
|
114
|
+
-- restores its exact runtime configuration instead of provider defaults.
|
|
115
115
|
model TEXT,
|
|
116
|
+
effort TEXT,
|
|
116
117
|
isArchived BOOLEAN DEFAULT 0,
|
|
117
118
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
118
119
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
@@ -138,6 +139,27 @@ CREATE TABLE IF NOT EXISTS app_config (
|
|
|
138
139
|
);
|
|
139
140
|
`;
|
|
140
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Persistent custom-model library used by the Providers module.
|
|
144
|
+
*
|
|
145
|
+
* Only user-created models are stored here. Predefined models remain source-
|
|
146
|
+
* controlled in each provider's `-models.provider.ts` adapter so they can be
|
|
147
|
+
* updated without migrating application data. `model_id` is unique only within
|
|
148
|
+
* a provider because different CLIs can accept the same identifier.
|
|
149
|
+
*/
|
|
150
|
+
export const PROVIDER_MODELS_TABLE_SCHEMA_SQL = `
|
|
151
|
+
CREATE TABLE IF NOT EXISTS provider_models (
|
|
152
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
153
|
+
provider TEXT NOT NULL CHECK (provider IN ('claude', 'cursor', 'codex', 'opencode')),
|
|
154
|
+
model_id TEXT NOT NULL,
|
|
155
|
+
model_name TEXT NOT NULL,
|
|
156
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
157
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
158
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
159
|
+
UNIQUE(provider, model_id)
|
|
160
|
+
);
|
|
161
|
+
`;
|
|
162
|
+
|
|
141
163
|
export const INIT_SCHEMA_SQL = `
|
|
142
164
|
-- Initialize authentication database
|
|
143
165
|
PRAGMA foreign_keys = ON;
|
|
@@ -181,4 +203,8 @@ CREATE INDEX IF NOT EXISTS idx_session_ids_lookup ON sessions(session_id);
|
|
|
181
203
|
${LAST_SCANNED_AT_SQL}
|
|
182
204
|
|
|
183
205
|
${APP_CONFIG_TABLE_SCHEMA_SQL}
|
|
206
|
+
|
|
207
|
+
${PROVIDER_MODELS_TABLE_SCHEMA_SQL}
|
|
208
|
+
CREATE INDEX IF NOT EXISTS idx_provider_models_provider_order
|
|
209
|
+
ON provider_models(provider, sort_order, id);
|
|
184
210
|
`;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
closeConnection,
|
|
9
|
+
getConnection,
|
|
10
|
+
initializeDatabase,
|
|
11
|
+
providerModelsDb,
|
|
12
|
+
sessionsDb,
|
|
13
|
+
} from '@/modules/database/index.js';
|
|
14
|
+
import { runMigrations } from '@/modules/database/migrations.js';
|
|
15
|
+
|
|
16
|
+
test('provider model repository stores custom rows only and maintains session references', async () => {
|
|
17
|
+
const previousDatabasePath = process.env.DATABASE_PATH;
|
|
18
|
+
const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'provider-model-db-'));
|
|
19
|
+
const databasePath = path.join(tempDirectory, 'auth.db');
|
|
20
|
+
|
|
21
|
+
closeConnection();
|
|
22
|
+
process.env.DATABASE_PATH = databasePath;
|
|
23
|
+
await writeFile(databasePath, '');
|
|
24
|
+
await initializeDatabase();
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const columns = getConnection().prepare('PRAGMA table_info(provider_models)').all() as Array<{
|
|
28
|
+
name: string;
|
|
29
|
+
}>;
|
|
30
|
+
assert.deepEqual(columns.map((column) => column.name), [
|
|
31
|
+
'id',
|
|
32
|
+
'provider',
|
|
33
|
+
'model_id',
|
|
34
|
+
'model_name',
|
|
35
|
+
'sort_order',
|
|
36
|
+
'created_at',
|
|
37
|
+
'updated_at',
|
|
38
|
+
]);
|
|
39
|
+
assert.deepEqual(providerModelsDb.listCustomProviderModels('codex'), []);
|
|
40
|
+
|
|
41
|
+
const custom = providerModelsDb.createCustomProviderModel('codex', {
|
|
42
|
+
model: 'Private Gateway Model',
|
|
43
|
+
id: 'gateway/model-v1',
|
|
44
|
+
});
|
|
45
|
+
assert.equal(custom.modelId, 'gateway/model-v1');
|
|
46
|
+
assert.equal(
|
|
47
|
+
providerModelsDb.findCustomProviderModelByModelId('codex', 'gateway/model-v1')?.recordId,
|
|
48
|
+
custom.recordId,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const db = getConnection();
|
|
52
|
+
db.prepare(`
|
|
53
|
+
INSERT INTO projects (project_id, project_path)
|
|
54
|
+
VALUES ('project-1', '/tmp/project-1')
|
|
55
|
+
`).run();
|
|
56
|
+
db.prepare(`
|
|
57
|
+
INSERT INTO sessions (session_id, provider, project_path, model, effort)
|
|
58
|
+
VALUES ('session-1', 'codex', '/tmp/project-1', 'gateway/model-v1', 'high')
|
|
59
|
+
`).run();
|
|
60
|
+
|
|
61
|
+
const updated = providerModelsDb.updateCustomProviderModel('codex', custom.recordId, {
|
|
62
|
+
model: 'Private Gateway Model 2',
|
|
63
|
+
id: 'gateway/model-v2',
|
|
64
|
+
});
|
|
65
|
+
assert.equal(updated?.modelId, 'gateway/model-v2');
|
|
66
|
+
assert.equal(sessionsDb.getSessionById('session-1')?.model, 'gateway/model-v2');
|
|
67
|
+
// A rename keeps the same underlying model, so its effort stays applicable.
|
|
68
|
+
assert.equal(sessionsDb.getSessionById('session-1')?.effort, 'high');
|
|
69
|
+
|
|
70
|
+
const removed = providerModelsDb.deleteCustomProviderModel(
|
|
71
|
+
'codex',
|
|
72
|
+
custom.recordId,
|
|
73
|
+
'gpt-default',
|
|
74
|
+
);
|
|
75
|
+
assert.equal(removed?.modelId, 'gateway/model-v2');
|
|
76
|
+
assert.equal(sessionsDb.getSessionById('session-1')?.model, 'gpt-default');
|
|
77
|
+
// The effort belonged to the deleted model and must not survive onto the
|
|
78
|
+
// fallback, which has its own default.
|
|
79
|
+
assert.equal(sessionsDb.getSessionById('session-1')?.effort, null);
|
|
80
|
+
assert.equal(providerModelsDb.getCustomProviderModel('codex', custom.recordId), null);
|
|
81
|
+
} finally {
|
|
82
|
+
closeConnection();
|
|
83
|
+
if (previousDatabasePath === undefined) {
|
|
84
|
+
delete process.env.DATABASE_PATH;
|
|
85
|
+
} else {
|
|
86
|
+
process.env.DATABASE_PATH = previousDatabasePath;
|
|
87
|
+
}
|
|
88
|
+
await rm(tempDirectory, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('migrations create the provider model index on an install that lacks it', async () => {
|
|
93
|
+
const previousDatabasePath = process.env.DATABASE_PATH;
|
|
94
|
+
const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'provider-model-index-'));
|
|
95
|
+
const databasePath = path.join(tempDirectory, 'auth.db');
|
|
96
|
+
|
|
97
|
+
closeConnection();
|
|
98
|
+
process.env.DATABASE_PATH = databasePath;
|
|
99
|
+
await writeFile(databasePath, '');
|
|
100
|
+
await initializeDatabase();
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const db = getConnection();
|
|
104
|
+
// Upgraded installs reach runMigrations with provider_models present but no
|
|
105
|
+
// index, so the CREATE INDEX statement is compiled against the real table
|
|
106
|
+
// instead of short-circuiting on the existing index name.
|
|
107
|
+
db.exec('DROP INDEX IF EXISTS idx_provider_models_provider_order');
|
|
108
|
+
|
|
109
|
+
runMigrations(db);
|
|
110
|
+
|
|
111
|
+
const indexedColumns = (db
|
|
112
|
+
.prepare('PRAGMA index_info(idx_provider_models_provider_order)')
|
|
113
|
+
.all() as Array<{ name: string }>)
|
|
114
|
+
.map((column) => column.name);
|
|
115
|
+
assert.deepEqual(indexedColumns, ['provider', 'sort_order', 'id']);
|
|
116
|
+
} finally {
|
|
117
|
+
closeConnection();
|
|
118
|
+
if (previousDatabasePath === undefined) {
|
|
119
|
+
delete process.env.DATABASE_PATH;
|
|
120
|
+
} else {
|
|
121
|
+
process.env.DATABASE_PATH = previousDatabasePath;
|
|
122
|
+
}
|
|
123
|
+
await rm(tempDirectory, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
@@ -45,7 +45,7 @@ test('disk-discovered sessions are keyed by the provider id for both columns', a
|
|
|
45
45
|
|
|
46
46
|
test('app sessions get the provider id assigned without creating a duplicate row', async () => {
|
|
47
47
|
await withIsolatedDatabase(() => {
|
|
48
|
-
sessionsDb.createAppSession('app-id-1', 'claude', '/workspace/demo');
|
|
48
|
+
sessionsDb.createAppSession('app-id-1', 'claude', '/workspace/demo', 'Initial CloudCLI message');
|
|
49
49
|
sessionsDb.assignProviderSessionId('app-id-1', 'provider-xyz');
|
|
50
50
|
|
|
51
51
|
// A later synchronizer pass that discovers the transcript on disk must
|
|
@@ -66,6 +66,7 @@ test('app sessions get the provider id assigned without creating a duplicate row
|
|
|
66
66
|
const row = sessionsDb.getSessionById('app-id-1');
|
|
67
67
|
assert.equal(row?.provider_session_id, 'provider-xyz');
|
|
68
68
|
assert.equal(row?.jsonl_path, '/fake/path/provider-xyz.jsonl');
|
|
69
|
+
assert.equal(row?.custom_name, 'Initial CloudCLI message');
|
|
69
70
|
});
|
|
70
71
|
});
|
|
71
72
|
|
|
@@ -6,6 +6,7 @@ import test from 'node:test';
|
|
|
6
6
|
|
|
7
7
|
import { closeConnection } from '@/modules/database/connection.js';
|
|
8
8
|
import { initializeDatabase } from '@/modules/database/init-db.js';
|
|
9
|
+
import { projectsDb } from '@/modules/database/repositories/projects.db.js';
|
|
9
10
|
import { sessionsDb } from '@/modules/database/repositories/sessions.db.js';
|
|
10
11
|
|
|
11
12
|
async function withIsolatedDatabase(runTest: () => void | Promise<void>): Promise<void> {
|
|
@@ -82,3 +83,34 @@ test('repository reads normalize SQLite UTC timestamps to ISO strings', async ()
|
|
|
82
83
|
assert.match(row?.updated_at ?? '', /^\d{4}-\d{2}-\d{2}T/);
|
|
83
84
|
});
|
|
84
85
|
});
|
|
86
|
+
|
|
87
|
+
test('recent sessions are globally ordered, paginated, and limited to visible conversations', async () => {
|
|
88
|
+
await withIsolatedDatabase(() => {
|
|
89
|
+
const fixtures: Array<Parameters<typeof sessionsDb.createSession>> = [
|
|
90
|
+
['session-oldest', 'claude', '/workspace/project-a', 'Oldest', '2026-07-18T09:00:00.000Z', '2026-07-18T10:00:00.000Z'],
|
|
91
|
+
['session-newest', 'codex', '/workspace/project-b', 'Newest', '2026-07-18T11:00:00.000Z', '2026-07-18T12:00:00.900Z'],
|
|
92
|
+
['session-same-second', 'claude', '/workspace/project-a', 'Same second, slightly older', '2026-07-18T12:00:00.000Z', '2026-07-18T12:00:00.100Z'],
|
|
93
|
+
['session-middle', 'claude', '/workspace/project-a', 'Middle', '2026-07-18T10:00:00.000Z', '2026-07-18T11:00:00.000Z'],
|
|
94
|
+
['session-archived', 'claude', '/workspace/project-a', 'Archived session', '2026-07-18T13:00:00.000Z', '2026-07-18T13:00:00.000Z'],
|
|
95
|
+
['session-hidden-project', 'claude', '/workspace/project-hidden', 'Archived project session', '2026-07-18T14:00:00.000Z', '2026-07-18T14:00:00.000Z'],
|
|
96
|
+
];
|
|
97
|
+
fixtures.forEach((fixture) => sessionsDb.createSession(...fixture));
|
|
98
|
+
|
|
99
|
+
sessionsDb.updateSessionIsArchived('session-archived', true);
|
|
100
|
+
projectsDb.updateProjectIsArchived('/workspace/project-hidden', true);
|
|
101
|
+
|
|
102
|
+
const firstPage = sessionsDb.getRecentSessionsPage(2, 0);
|
|
103
|
+
const secondPage = sessionsDb.getRecentSessionsPage(2, 2);
|
|
104
|
+
|
|
105
|
+
assert.equal(firstPage.total, 4);
|
|
106
|
+
assert.deepEqual(
|
|
107
|
+
firstPage.sessions.map((session) => session.session_id),
|
|
108
|
+
['session-newest', 'session-same-second'],
|
|
109
|
+
);
|
|
110
|
+
assert.equal(secondPage.total, 4);
|
|
111
|
+
assert.deepEqual(
|
|
112
|
+
secondPage.sessions.map((session) => session.session_id),
|
|
113
|
+
['session-middle', 'session-oldest'],
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -36,7 +36,11 @@ const fileTreeFileSystem: FileTreeFileSystem = {
|
|
|
36
36
|
access: (candidatePath) => fsPromises.access(candidatePath),
|
|
37
37
|
stat: (candidatePath) => fsPromises.stat(candidatePath),
|
|
38
38
|
lstat: (candidatePath) => fsPromises.lstat(candidatePath),
|
|
39
|
-
|
|
39
|
+
// `opendir` streams entries in batches and the handle is closed by the
|
|
40
|
+
// iterator protocol, including when the caller stops early at the entry cap.
|
|
41
|
+
openDirectory: async function* (directoryPath) {
|
|
42
|
+
yield* await fsPromises.opendir(directoryPath);
|
|
43
|
+
},
|
|
40
44
|
realpath: (candidatePath) => fsPromises.realpath(candidatePath),
|
|
41
45
|
readTextFile: (filePath) => fsPromises.readFile(filePath, 'utf8'),
|
|
42
46
|
writeTextFile: (filePath, content) => fsPromises.writeFile(filePath, content, 'utf8'),
|
|
@@ -3,16 +3,21 @@ import path from 'node:path';
|
|
|
3
3
|
import ignore from 'ignore';
|
|
4
4
|
|
|
5
5
|
import type {
|
|
6
|
+
FileTreeDirectoryEntry,
|
|
6
7
|
FileTreeNode,
|
|
7
8
|
FileTreeServiceDependencies,
|
|
8
9
|
FileTreeServices,
|
|
9
10
|
FileTreeUploadedFile,
|
|
10
11
|
} from '@/shared/types.js';
|
|
11
|
-
import { AppError } from '@/shared/utils.js';
|
|
12
|
+
import { AppError, FORBIDDEN_WORKSPACE_PATHS, normalizeProjectPath } from '@/shared/utils.js';
|
|
13
|
+
|
|
14
|
+
const HARD_EXCLUDED_DIRECTORY_NAMES = new Set([
|
|
15
|
+
'node_modules', '.git', '.svn', '.hg',
|
|
16
|
+
]);
|
|
12
17
|
|
|
13
18
|
const IGNORED_DIRECTORY_NAMES = new Set([
|
|
14
|
-
|
|
15
|
-
'.
|
|
19
|
+
...HARD_EXCLUDED_DIRECTORY_NAMES,
|
|
20
|
+
'dist', 'build', '.next', '.nuxt', '.cache', '.parcel-cache',
|
|
16
21
|
'__pycache__', '.pytest_cache', '.mypy_cache', '.tox', 'venv', '.venv',
|
|
17
22
|
'target', 'vendor',
|
|
18
23
|
'.gradle', '.idea', 'coverage', '.nyc_output',
|
|
@@ -28,8 +33,22 @@ const COMMON_WORKSPACE_DIRECTORY_NAMES = [
|
|
|
28
33
|
'workspace',
|
|
29
34
|
];
|
|
30
35
|
|
|
36
|
+
// File Tree consumes this guard when recursively listing a project so a very
|
|
37
|
+
// broad workspace (for example, a user's home directory) cannot exhaust the
|
|
38
|
+
// server heap before the browser has a chance to switch to a narrower project.
|
|
39
|
+
const MAXIMUM_FILE_TREE_ENTRIES = 10_000;
|
|
40
|
+
|
|
31
41
|
type FileTreeEntryFilter = (entryPath: string, isDirectory: boolean) => boolean;
|
|
32
42
|
|
|
43
|
+
function includeEntryByHardExclusions(entryPath: string, isDirectory: boolean): boolean {
|
|
44
|
+
return !isDirectory || !HARD_EXCLUDED_DIRECTORY_NAMES.has(path.basename(entryPath));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function includeEntryByFallbackDirectoryNames(entryPath: string, isDirectory: boolean): boolean {
|
|
48
|
+
return includeEntryByHardExclusions(entryPath, isDirectory)
|
|
49
|
+
&& (!isDirectory || !IGNORED_DIRECTORY_NAMES.has(path.basename(entryPath)));
|
|
50
|
+
}
|
|
51
|
+
|
|
33
52
|
function createFileTreeError(message: string, statusCode: number, code: string): AppError {
|
|
34
53
|
return new AppError(message, { statusCode, code });
|
|
35
54
|
}
|
|
@@ -140,6 +159,8 @@ function createGitignoreEntryFilter(
|
|
|
140
159
|
const gitignore = ignore().add(gitignoreContent);
|
|
141
160
|
|
|
142
161
|
return (entryPath, isDirectory) => {
|
|
162
|
+
if (!includeEntryByHardExclusions(entryPath, isDirectory)) return false;
|
|
163
|
+
|
|
143
164
|
const relativePath = path.relative(projectRoot, entryPath).split(path.sep).join('/');
|
|
144
165
|
const matchPath = isDirectory ? `${relativePath}/` : relativePath;
|
|
145
166
|
return !gitignore.ignores(matchPath);
|
|
@@ -167,35 +188,66 @@ export function createFileTreeService(dependencies: FileTreeServiceDependencies)
|
|
|
167
188
|
return projectRoot;
|
|
168
189
|
}
|
|
169
190
|
|
|
170
|
-
|
|
191
|
+
/**
|
|
192
|
+
* Streams one directory and keeps only the entries the tree will show.
|
|
193
|
+
*
|
|
194
|
+
* Entries are filtered and charged against the shared budget one at a time,
|
|
195
|
+
* so a pathologically large directory stops the walk at the cap instead of
|
|
196
|
+
* being read into memory in full first.
|
|
197
|
+
*/
|
|
198
|
+
async function collectVisibleEntries(
|
|
171
199
|
directoryPath: string,
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
200
|
+
includeEntry: FileTreeEntryFilter,
|
|
201
|
+
remainingEntries: { value: number },
|
|
202
|
+
): Promise<FileTreeDirectoryEntry[]> {
|
|
203
|
+
const visibleEntries: FileTreeDirectoryEntry[] = [];
|
|
204
|
+
|
|
205
|
+
await acquire();
|
|
177
206
|
try {
|
|
178
|
-
await
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
207
|
+
for await (const entry of fileSystem.openDirectory(directoryPath)) {
|
|
208
|
+
const isDirectory = entry.isDirectory();
|
|
209
|
+
if (!includeEntry(path.join(directoryPath, entry.name), isDirectory)) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Charged after visibility filtering so ignored entries never consume
|
|
214
|
+
// the budget. Every recursive branch shares one counter, so the cap
|
|
215
|
+
// applies to the whole tree rather than per directory.
|
|
216
|
+
if (remainingEntries.value <= 0) {
|
|
217
|
+
throw createFileTreeError(
|
|
218
|
+
`Project file tree exceeds the ${MAXIMUM_FILE_TREE_ENTRIES.toLocaleString()} entry limit. Choose a narrower project directory or add ignore rules.`,
|
|
219
|
+
413,
|
|
220
|
+
'FILE_TREE_TOO_LARGE',
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
remainingEntries.value -= 1;
|
|
224
|
+
visibleEntries.push(entry);
|
|
183
225
|
}
|
|
184
226
|
} catch (error) {
|
|
227
|
+
// The entry cap is a caller-visible outcome, not an unreadable directory.
|
|
228
|
+
if (error instanceof AppError) {
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
185
231
|
const errorCode = readErrorCode(error);
|
|
186
232
|
if (errorCode !== 'EACCES' && errorCode !== 'EPERM') {
|
|
187
233
|
dependencies.logger.error(`Error reading directory "${directoryPath}"`, error);
|
|
188
234
|
}
|
|
189
235
|
return [];
|
|
236
|
+
} finally {
|
|
237
|
+
release();
|
|
190
238
|
}
|
|
191
239
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
240
|
+
return visibleEntries;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function buildFileTree(
|
|
244
|
+
directoryPath: string,
|
|
245
|
+
maximumDepth: number,
|
|
246
|
+
currentDepth = 0,
|
|
247
|
+
includeEntry: FileTreeEntryFilter = includeEntryByFallbackDirectoryNames,
|
|
248
|
+
remainingEntries = { value: MAXIMUM_FILE_TREE_ENTRIES },
|
|
249
|
+
): Promise<FileTreeNode[]> {
|
|
250
|
+
const visibleEntries = await collectVisibleEntries(directoryPath, includeEntry, remainingEntries);
|
|
199
251
|
|
|
200
252
|
const items = await Promise.all(visibleEntries.map(async (entry): Promise<FileTreeNode> => {
|
|
201
253
|
const itemPath = path.join(directoryPath, entry.name);
|
|
@@ -233,12 +285,19 @@ export function createFileTreeService(dependencies: FileTreeServiceDependencies)
|
|
|
233
285
|
// Metadata failures should not hide an otherwise readable tree entry.
|
|
234
286
|
}
|
|
235
287
|
|
|
236
|
-
|
|
288
|
+
// Skip recursing into pseudo-filesystems and other system-critical
|
|
289
|
+
// directories (e.g. /proc, /sys) — they're never valid project roots,
|
|
290
|
+
// and /proc in particular can contain thousands of virtual entries
|
|
291
|
+
// that make traversal from a broad root (e.g. "/") pathologically slow.
|
|
292
|
+
const isForbiddenSystemDir = FORBIDDEN_WORKSPACE_PATHS.includes(normalizeProjectPath(itemPath));
|
|
293
|
+
|
|
294
|
+
if (entry.isDirectory() && currentDepth < maximumDepth && !isForbiddenSystemDir) {
|
|
237
295
|
item.children = await buildFileTree(
|
|
238
296
|
itemPath,
|
|
239
297
|
maximumDepth,
|
|
240
298
|
currentDepth + 1,
|
|
241
299
|
includeEntry,
|
|
300
|
+
remainingEntries,
|
|
242
301
|
);
|
|
243
302
|
}
|
|
244
303
|
|