Skip to content

Commit 354dcd2

Browse files
committed
fix: scope project Skiller browser picker
1 parent 11dac2e commit 354dcd2

7 files changed

Lines changed: 110 additions & 10 deletions

File tree

docs/integrations/skiller.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ The latest Playwright proof screenshots are checked in under `docs/screenshots/i
5858
- `pr238-proof-28-skiller-session-scoped-ui.png` shows the real Skiller UI opened from that button.
5959
- `pr238-proof-29-skiller-codex-container-skill.png` shows a Codex skill discovered from the selected container's `/home/dev/.codex/skills` tree.
6060
- `pr238-proof-30-skiller-add-project-folder-browser-picker.png` shows the browser-visible project folder picker opened from `Add project folder...` with selected-container paths.
61+
- `pr238-proof-31-skiller-project-scoped-folder-picker-working.png` shows `/api/skiller/app/` opened from a project-scoped Skiller process with `Add project folder...` resolving to the selected container paths.
62+
- `pr238-proof-32-docker-git-browser-live-command.png` shows the live `docker-git browser` frontend served through the same controller used for the proof.
6163

6264
## Updating the Pin
6365

92.1 KB
Loading
54 KB
Loading

packages/api/src/services/skiller-core.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export type SkillerBrowserScope = {
3131
readonly currentProject: SkillerBrowserPathRoot
3232
readonly projectKey: string
3333
readonly roots: ReadonlyArray<SkillerBrowserPathRoot>
34-
readonly sessionId: string
34+
readonly sessionId: string | null
3535
}
3636

3737
export const parseDockerMountLines = (output: string): ReadonlyArray<DockerContainerMount> =>
@@ -142,7 +142,7 @@ const rootDepth = (root: Pick<SkillerBrowserPathRoot, "containerPath" | "hostPat
142142

143143
export const skillerBrowserScopeForContainer = (
144144
scope: SkillerContainerScope,
145-
sessionId: string
145+
sessionId: string | null
146146
): SkillerBrowserScope => {
147147
const roots = skillerBrowserRoots(scope)
148148
return {

packages/api/src/services/skiller.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,15 @@ type SkillerProcessUser = {
5353
readonly uid: number
5454
}
5555

56-
type SkillerRoute =
56+
export type SkillerRoute =
5757
| { readonly _tag: "App"; readonly relativePath: string; readonly sessionId: string | null }
5858
| { readonly _tag: "Trpc"; readonly sessionId: string | null; readonly upstreamPath: string }
5959

60+
type SkillerBrowserScopeSelection = {
61+
readonly scope: SkillerContainerScope
62+
readonly sessionId: string | null
63+
}
64+
6065
const submoduleRelativePath = join("third_party", "skiller-desktop-skills-manager")
6166
const launchLogPath = join(homedir(), ".docker-git", "logs", "skiller.log")
6267
const skillerAppPath = "/api/skiller/app/"
@@ -586,19 +591,39 @@ const browserTrpcBaseBootstrap = [
586591
const scriptJson = (value: unknown): string =>
587592
JSON.stringify(value).replaceAll("<", "\\u003c")
588593

594+
export const resolveSkillerBrowserScopeSelection = (
595+
route: Extract<SkillerRoute, { readonly _tag: "App" }>,
596+
currentScope: SkillerContainerScope | null,
597+
sessionScopeForId: (sessionId: string) => SkillerContainerScope | null | undefined
598+
): SkillerBrowserScopeSelection | null => {
599+
if (route.sessionId === null) {
600+
return currentScope === null
601+
? null
602+
: { scope: currentScope, sessionId: null }
603+
}
604+
const sessionScope = sessionScopeForId(route.sessionId)
605+
return sessionScope === undefined || sessionScope === null
606+
? null
607+
: { scope: sessionScope, sessionId: route.sessionId }
608+
}
609+
589610
const browserDockerGitScopeBootstrap = (
590611
route: Extract<SkillerRoute, { readonly _tag: "App" }>
591612
): string => {
592-
if (route.sessionId === null) {
593-
return ""
594-
}
595-
const scope = sessionScopes.get(route.sessionId)
596-
if (scope === undefined || scope === null) {
613+
const currentScope = currentProcess !== null && isRunning(currentProcess.process)
614+
? currentProcess.scope
615+
: null
616+
const selection = resolveSkillerBrowserScopeSelection(
617+
route,
618+
currentScope,
619+
(sessionId) => sessionScopes.get(sessionId)
620+
)
621+
if (selection === null) {
597622
return ""
598623
}
599624
return [
600625
"<script>",
601-
`window.__DOCKER_GIT_SKILLER_SCOPE__=${scriptJson(skillerBrowserScopeForContainer(scope, route.sessionId))};`,
626+
`window.__DOCKER_GIT_SKILLER_SCOPE__=${scriptJson(skillerBrowserScopeForContainer(selection.scope, selection.sessionId))};`,
602627
"</script>"
603628
].join("")
604629
}

packages/api/tests/skiller-core.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,18 @@ describe("skiller container filesystem mapping", () => {
6868
sshUser: "dev"
6969
}, "terminal-session")
7070

71+
expect(skillerBrowserScopeForContainer({
72+
containerCodexSkillsPath: "/home/dev/.codex/skills",
73+
containerHomePath: "/home/dev",
74+
containerName: "dg-project",
75+
containerProjectPath: "/home/dev/app",
76+
hostCodexSkillsPath: "/var/lib/docker/volumes/project-home/_data/.codex/skills",
77+
hostHomePath: "/var/lib/docker/volumes/project-home/_data",
78+
hostProjectPath: "/var/lib/docker/volumes/project-home/_data/app",
79+
projectId: "/home/dev/.docker-git/project",
80+
projectKey: "abc123",
81+
sshUser: "dev"
82+
}, null).sessionId).toBeNull()
7183
expect(browserScope.currentProject.containerPath).toBe("/home/dev/app")
7284
expect(remapSkillerBrowserContainerPath(browserScope, "/home/dev/app/packages")).toBe(
7385
"/var/lib/docker/volumes/project-home/_data/app/packages"

packages/api/tests/skiller-routes.test.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,32 @@
11
import { describe, expect, it } from "@effect/vitest"
22

3-
import { parseSkillerRoute } from "../src/services/skiller.js"
3+
import {
4+
parseSkillerRoute,
5+
resolveSkillerBrowserScopeSelection,
6+
type SkillerRoute
7+
} from "../src/services/skiller.js"
8+
import type { SkillerContainerScope } from "../src/services/skiller-core.js"
9+
10+
const appRoute = (path: string): Extract<SkillerRoute, { readonly _tag: "App" }> => {
11+
const route = parseSkillerRoute(path)
12+
if (route === null || route._tag !== "App") {
13+
throw new Error(`Expected app route for ${path}.`)
14+
}
15+
return route
16+
}
17+
18+
const scope = (projectKey: string): SkillerContainerScope => ({
19+
containerCodexSkillsPath: "/home/dev/.codex/skills",
20+
containerHomePath: "/home/dev",
21+
containerName: `dg-${projectKey}`,
22+
containerProjectPath: "/home/dev/app",
23+
hostCodexSkillsPath: `/var/lib/docker/volumes/${projectKey}-home/_data/.codex/skills`,
24+
hostHomePath: `/var/lib/docker/volumes/${projectKey}-home/_data`,
25+
hostProjectPath: `/var/lib/docker/volumes/${projectKey}-home/_data/app`,
26+
projectId: `/home/dev/.docker-git/${projectKey}`,
27+
projectKey,
28+
sshUser: "dev"
29+
})
430

531
describe("skiller routes", () => {
632
it("keeps the terminal session id on session-scoped app routes", () => {
@@ -20,4 +46,39 @@ describe("skiller routes", () => {
2046
sessionId: null
2147
})
2248
})
49+
50+
it("uses the current project scope for non-session app routes", () => {
51+
const currentScope = scope("project-scope")
52+
53+
expect(resolveSkillerBrowserScopeSelection(
54+
appRoute("/api/skiller/app/"),
55+
currentScope,
56+
() => null
57+
)).toEqual({
58+
scope: currentScope,
59+
sessionId: null
60+
})
61+
})
62+
63+
it("uses the registered terminal scope for session app routes", () => {
64+
const currentScope = scope("current-scope")
65+
const terminalScope = scope("terminal-scope")
66+
67+
expect(resolveSkillerBrowserScopeSelection(
68+
appRoute("/api/ssh/session/terminal-proof/skiller/app/"),
69+
currentScope,
70+
(sessionId) => sessionId === "terminal-proof" ? terminalScope : null
71+
)).toEqual({
72+
scope: terminalScope,
73+
sessionId: "terminal-proof"
74+
})
75+
})
76+
77+
it("does not inject a browser scope for unscoped Skiller app routes", () => {
78+
expect(resolveSkillerBrowserScopeSelection(
79+
appRoute("/api/skiller/app/"),
80+
null,
81+
() => null
82+
)).toBeNull()
83+
})
2384
})

0 commit comments

Comments
 (0)