Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
e963f02
WIP: same-host same-remote checkouts keep their own project id
brennanb2025 Oct 14, 2026
a3d071a
WIP: claim the shared project id per checkout surface; drop the displ…
brennanb2025 Oct 14, 2026
b4d1dda
WIP: index derived ids by repo in the hydration normalizer
brennanb2025 Oct 14, 2026
028247c
Merge current main into history folder identity work
brennanb2025 Oct 23, 2026
ab86447
Resolve workspace project affiliation from current checkout owners
brennanb2025 Oct 23, 2026
e24b8fa
fix: preserve concrete project ownership across catalogs and operations
brennanb2025 Oct 23, 2026
250e28d
fix: combine recent project selector imports
brennanb2025 Oct 23, 2026
72c2d2a
fix: enforce canonical catalog reads and preserve search continuity
brennanb2025 Oct 23, 2026
fbdfb03
fix: scope catalog lint exceptions to renderer files
brennanb2025 Oct 23, 2026
ebcaed6
Resolve project search ownership before native candidate limits
brennanb2025 Oct 23, 2026
970c858
Combine duplicate session scope imports
brennanb2025 Oct 23, 2026
1310738
fix: derive returned history row ownership on the host
brennanb2025 Oct 23, 2026
0c0aaf2
Reconcile full session history replies by content
brennanb2025 Oct 23, 2026
768759b
Require authoritative project catalogs for terminal snapshots
brennanb2025 Oct 23, 2026
15e319a
Preserve registered project claims and partial history results
brennanb2025 Oct 24, 2026
2cc1bee
Merge origin/main into history project ownership branch
brennanb2025 Oct 24, 2026
d48531b
test: check orphaned setup cleanup state without assertions
brennanb2025 Oct 24, 2026
1489c6f
fix: align catalog callers and native search fixture contracts
brennanb2025 Oct 24, 2026
1e3786c
Merge origin/main to resolve integration conflict
brennanb2025 Oct 24, 2026
93d61b4
test: check full repo shapes in runtime routing fixtures
brennanb2025 Oct 24, 2026
36c0245
Fix checked catalog inputs in compiler regression fixtures
brennanb2025 Oct 24, 2026
cd44b09
Keep checked PTY fixture repos mutable
brennanb2025 Oct 24, 2026
43f13a6
test: align catalog fixtures with canonical readers
brennanb2025 Oct 24, 2026
54b072d
test: check catalog and runtime fixture inputs
brennanb2025 Oct 24, 2026
f2323e0
Merge origin/main into history folder identity branch
brennanb2025 Oct 24, 2026
0aa1dd2
test: check catalog fixtures without outer assertions
brennanb2025 Oct 24, 2026
460e437
test: follow persistence harness relocation
brennanb2025 Oct 24, 2026
eb00d0e
Merge origin/main into history folder identity branch
brennanb2025 Oct 25, 2026
697b6c9
Adapt catalog test inputs to integrated main checks
brennanb2025 Oct 25, 2026
3c2d762
Merge main and preserve canonical History ownership
brennanb2025 Oct 26, 2026
a4e4e8d
Test History hashing across cached depth revalidation
brennanb2025 Oct 26, 2026
8b380ab
Merge main to retain current project routing and catalog checks
brennanb2025 Oct 26, 2026
cad26c0
test(settings): expect host-qualified setup removal
brennanb2025 Oct 26, 2026
db916af
Merge origin/main into history folder identity branch
brennanb2025 Oct 26, 2026
d34e32c
Merge origin/main into history folder identity branch
brennanb2025 Oct 26, 2026
cfb71e3
Merge origin/main into history folder identity branch
brennanb2025 Oct 26, 2026
6dfc890
Merge main into history folder identity branch
brennanb2025 Oct 26, 2026
268221c
Merge main into history folder identity branch
brennanb2025 Oct 26, 2026
0c8bd2e
test: capture host split listings and scope relay recovery
brennanb2025 Oct 26, 2026
0354eed
Merge main; preserve catalog fixtures and host split diagnostics
brennanb2025 Oct 26, 2026
97cb0f4
ci: regenerate merge ref
brennanb2025 Oct 26, 2026
7b7d10a
Merge main and preserve scoped relay recovery test
brennanb2025 Oct 26, 2026
2f6dded
Merge main into history folder identity integration
brennanb2025 Oct 27, 2026
eeb13f9
Merge main and preserve catalog lint boundary
brennanb2025 Oct 27, 2026
0857732
test: type host-split listing diagnostics
brennanb2025 Oct 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
41 changes: 41 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,14 @@
{
"property": "randomUUID",
"message": "crypto.randomUUID is missing in non-secure contexts (Remote Web over plain HTTP), which white-screens the app. Use createBrowserUuid() from '@/lib/browser-uuid'."
},
{
"property": "endpointProjects",
"message": "Read canonical affiliation through project-host-setup-selector; endpoint facts are reserved for ingestion and concrete operation adapters."
},
{
"property": "endpointProjectHostSetups",
"message": "Read canonical affiliation through project-host-setup-selector; endpoint facts are reserved for ingestion and concrete operation adapters."
}
]
}
Expand Down Expand Up @@ -207,6 +215,39 @@
"rules": {
"max-lines": ["error", { "max": 800, "skipBlankLines": true, "skipComments": true }]
}
},
{
"files": [
"src/renderer/src/store/project-host-setup-selector.ts",
"src/renderer/src/store/project-search-catalog.ts",
"src/renderer/src/store/repos/repo-catalog-identity.ts",
"src/renderer/src/store/repos/repo-catalog-actions.ts",
"src/renderer/src/store/repos/runtime-repo-catalog-actions.ts",
"src/renderer/src/store/repos/all-host-repo-catalog-actions.ts",
"src/renderer/src/store/repos/repo-add-actions.ts",
"src/renderer/src/store/repos/repo-update.ts",
"src/renderer/src/store/repos/repo-removal.ts",
"src/renderer/src/store/projects/project-host-setup-actions.ts",
"src/renderer/src/store/projects/project-update.ts",
"src/renderer/src/store/project-groups/project-group-mutations.ts",
"src/renderer/src/store/slices/worktrees/teardown/purge-stale-runtime-host-state.ts",
"src/renderer/src/store/slices/worktrees/teardown/orphaned-runtime-ssh-project-purge.ts",
"src/renderer/src/app-shell/use-runtime-graph-sync.ts",
"src/renderer/src/**/*.test.{ts,tsx}",
"src/renderer/src/**/*.spec.{ts,tsx}",
"src/renderer/src/**/*-test-*.ts",
"src/renderer/src/**/*.test-fixture.ts",
"src/renderer/src/components/automations/automations-page-runtime-fixtures.ts"
],
"rules": {
"no-restricted-properties": [
"error",
{
"property": "randomUUID",
"message": "crypto.randomUUID is missing in non-secure contexts (Remote Web over plain HTTP), which white-screens the app. Use createBrowserUuid() from '@/lib/browser-uuid'."
}
]
}
}
],
"ignorePatterns": [
Expand Down
27 changes: 27 additions & 0 deletions config/oxlint-js-plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
{
"files": ["**/src/renderer/src/**/*.{ts,tsx}"],
"rules": {
"app-store-performance/no-raw-project-catalog-computed": "error",
"renderer-scrollbar-style/require-styled-vertical-scrollbar": "error",
"ime-enter-guard/require-ime-guard": "error"
}
Expand All @@ -49,6 +50,32 @@
"renderer-scrollbar-style/require-styled-vertical-scrollbar": "off",
"ime-enter-guard/require-ime-guard": "off"
}
},
{
"files": [
"src/renderer/src/store/project-host-setup-selector.ts",
"src/renderer/src/store/repos/repo-catalog-identity.ts",
"src/renderer/src/store/repos/repo-catalog-actions.ts",
"src/renderer/src/store/repos/runtime-repo-catalog-actions.ts",
"src/renderer/src/store/repos/all-host-repo-catalog-actions.ts",
"src/renderer/src/store/repos/repo-add-actions.ts",
"src/renderer/src/store/repos/repo-update.ts",
"src/renderer/src/store/repos/repo-removal.ts",
"src/renderer/src/store/projects/project-host-setup-actions.ts",
"src/renderer/src/store/projects/project-update.ts",
"src/renderer/src/store/project-groups/project-group-mutations.ts",
"src/renderer/src/store/slices/worktrees/teardown/purge-stale-runtime-host-state.ts",
"src/renderer/src/store/slices/worktrees/teardown/orphaned-runtime-ssh-project-purge.ts",
"src/renderer/src/app-shell/use-runtime-graph-sync.ts",
"src/renderer/src/**/*.test.{ts,tsx}",
"src/renderer/src/**/*.spec.{ts,tsx}",
"src/renderer/src/**/*-test-*.ts",
"src/renderer/src/**/*.test-fixture.ts",
"src/renderer/src/components/automations/automations-page-runtime-fixtures.ts"
],
"rules": {
"app-store-performance/no-raw-project-catalog-computed": "off"
}
}
],
"ignorePatterns": ["**/node_modules", "**/dist", "**/out"]
Expand Down
30 changes: 29 additions & 1 deletion config/oxlint-plugins/app-store-performance.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -354,12 +354,39 @@ function noNestedFreshUnderShallowRule(sourceCode) {
})
}

function noRawProjectCatalogComputedRule(sourceCode) {
return {
MemberExpression(node) {
if (!node.computed || node.property.type !== 'Identifier') {
return
}
const definition = variableFor(node.property, { sourceCode })?.defs[0]
const declaration = definition?.node
if (
definition?.type === 'Variable' &&
declaration.parent.kind === 'const' &&
declaration.init?.type === 'Literal' &&
['endpointProjects', 'endpointProjectHostSetups'].includes(declaration.init.value)
) {
this.report({
node,
message: 'Read canonical project affiliation through the project catalog selector.'
})
}
}
}
}

// Skip JS AST deserialization in files that cannot name a store hook.
const STORE_HOOK_TEXT = /use[A-Z][A-Za-z0-9]*Store/

function bindContext(createVisitors) {
return (context) => {
if (!STORE_HOOK_TEXT.test(context.sourceCode.text) && !context.sourceCode.text.includes('\\')) {
if (
createVisitors !== noRawProjectCatalogComputedRule &&
!STORE_HOOK_TEXT.test(context.sourceCode.text) &&
!context.sourceCode.text.includes('\\')
) {
return {}
}
const visitors = createVisitors(context.sourceCode)
Expand All @@ -374,6 +401,7 @@ export default {
meta: { name: 'app-store-performance' },
rules: {
'require-selector': { create: bindContext(requireSelectorRule) },
'no-raw-project-catalog-computed': { create: bindContext(noRawProjectCatalogComputedRule) },
'no-identity-selector': { create: bindContext(noIdentitySelectorRule) },
'no-fresh-selector-result': { create: bindContext(noFreshSelectorResultRule) },
'no-nested-fresh-under-shallow': { create: bindContext(noNestedFreshUnderShallowRule) }
Expand Down
102 changes: 102 additions & 0 deletions config/scripts/project-catalog-read-boundary.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { readFileSync, mkdtempSync, realpathSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { resolve, join, dirname } from 'node:path'
import { spawnSync } from 'node:child_process'
import { expect, it } from 'vitest'
import { resolveOxcCliInvocation } from './oxc-cli-invocation.mjs'

it('enforces five raw catalog forms with the actual config and narrow file exceptions', () => {
const config = JSON.parse(readFileSync(resolve('.oxlintrc.json'), 'utf8'))
const jsPluginConfig = JSON.parse(readFileSync(resolve('config/oxlint-js-plugins.json'), 'utf8'))
expect(config.ignorePatterns.every((pattern) => typeof pattern === 'string')).toBe(true)
const folder = realpathSync(mkdtempSync(join(tmpdir(), 'sta-10541-catalog-rule-')))
const source = `
const key = 'projects';
function read(state) {
state.endpointProjects;
state?.endpointProjectHostSetups;
const { endpointProjects: projects } = state;
state['endpointProjectHostSetups'];
const key = 'endpointProjects'; state[key];
{ const key = 'projects'; state[key]; }
function shadow(key) { return state[key]; }
const unrelated = 'projectHostSetups'; state[unrelated];
state.projects;
state.projectHostSetups;
return projects;
}
`
try {
writeFileSync(join(folder, 'config.json'), JSON.stringify(config))
writeFileSync(
join(folder, 'js-config.json'),
JSON.stringify({
...jsPluginConfig,
jsPlugins: jsPluginConfig.jsPlugins.map((plugin) => ({
...plugin,
specifier: resolve('config', plugin.specifier)
}))
})
)
const lint = (file, content = source) => {
const target = join(folder, file)
mkdirSync(dirname(target), { recursive: true })
writeFileSync(target, content)
return ['config.json', 'js-config.json'].flatMap((configFile) => {
const invocation = resolveOxcCliInvocation('oxlint', 'oxlint')
const result = spawnSync(
invocation.command,
[
...invocation.prefixArgs,
'--config',
join(folder, configFile),
'--format',
'json',
file
],
{ cwd: folder, encoding: 'utf8' }
)
expect(result.error).toBeUndefined()
const diagnostics = JSON.parse(result.stdout).diagnostics
expect(result.stderr).toBe('')
expect(JSON.parse(result.stdout).number_of_files, result.stdout).toBe(1)
return diagnostics.filter(
(entry) =>
entry.code.includes('no-restricted-properties') ||
entry.code.includes('no-raw-project-catalog-computed')
)
})
}
for (const file of [
'src/renderer/src/hooks/useActiveProjectSkillRuntime.ts',
'src/renderer/src/store/repos/repo-state.ts'
]) {
const forbidden = lint(file)
expect(forbidden).toHaveLength(5)
expect(
forbidden.filter((entry) => entry.code.includes('no-restricted-properties'))
).toHaveLength(4)
expect(
forbidden.filter((entry) => entry.code.includes('no-raw-project-catalog-computed'))
).toHaveLength(1)
}
const allowed = 'src/renderer/src/store/project-host-setup-selector.ts'
expect(lint(allowed)).toEqual([])
const uuid = lint(allowed, 'crypto.randomUUID()')
expect(uuid).toHaveLength(1)
expect(uuid[0].message).toContain('randomUUID')
for (const file of [
'src/main/native-chat/agent-session-wire/structured-agent-session-host-test-harness.ts',
'src/main/runtime/rpc/methods/structured-chat-tab-table.test-fixture.ts',
'mobile/src/test-support/rpc-recording/native-mounting-substitutes.test.ts',
'tests/e2e/worktree-smart-sort.spec.ts'
]) {
expect(lint(file, 'crypto.randomUUID()')).toEqual([])
}
const sharedUuid = lint('src/shared/tmux-status.test-fixture.ts', 'crypto.randomUUID()')
expect(sharedUuid).toHaveLength(1)
expect(sharedUuid[0].help).toContain('src/shared is compiled into the web bundle')
} finally {
rmSync(folder, { recursive: true, force: true })
}
})
24 changes: 19 additions & 5 deletions mobile/src/components/NewWorktreeModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useMemo, useState } from 'react'
import { Keyboard } from 'react-native'
import { getComposerRepoWorktreeBranches } from '../../../src/shared/composer-branch-selection'
import { getRepoExecutionHostId, parseExecutionHostId } from '../../../src/shared/execution-host'
import { getProjectIdentityKey } from '../../../src/shared/project-host-setup-projection'
import { getRepoHostIdentity } from '../../../src/shared/repo-host-identity'
import { shouldPreserveWorkspaceSourceOnRepoChange } from '../../../src/shared/new-workspace/workspace-source'
import type { SmartModeAvailabilityInput } from '../tasks/mobile-smart-source-modes'
import { deriveRepoSlug, type PasteRepoCandidate } from '../tasks/smart-source-paste-intent'
Expand All @@ -22,6 +22,7 @@ import {
type NewWorktreeModalProps
} from './new-worktree-modal-types'
import {
getNewWorkspaceProjectIds,
buildNewWorkspaceProjectOptions,
buildNewWorkspaceRunTargetOptions,
getNewWorkspaceRunTarget
Expand Down Expand Up @@ -178,13 +179,26 @@ function NewWorktreeModalContent(props: NewWorktreeModalProps) {
})),
[hostRepos]
)
const projectPickerItems = useMemo(() => buildNewWorkspaceProjectOptions(repos), [repos])
const selectedProjectId = selectedRepo ? getProjectIdentityKey(selectedRepo) : null
const projectIds = useMemo(() => getNewWorkspaceProjectIds(repos), [repos])
const projectPickerItems = useMemo(
() => buildNewWorkspaceProjectOptions(repos, projectIds),
[repos, projectIds]
)
const selectedProjectId = selectedRepo
? (projectIds.get(getRepoHostIdentity(selectedRepo)) ?? null)
: null
const selectedProject =
projectPickerItems.find((project) => project.id === selectedProjectId) ?? null
const runTargetPickerItems = useMemo(
() => buildNewWorkspaceRunTargetOptions(repos, selectedProjectId, hostPlatform, hostLabelById),
[hostLabelById, hostPlatform, repos, selectedProjectId]
() =>
buildNewWorkspaceRunTargetOptions(
repos,
selectedProjectId,
hostPlatform,
hostLabelById,
projectIds
),
[hostLabelById, hostPlatform, repos, selectedProjectId, projectIds]
)
const selectedRunTarget = selectedRepo
? getNewWorkspaceRunTarget(selectedRepo, hostPlatform, hostLabelById)
Expand Down
16 changes: 14 additions & 2 deletions mobile/src/components/new-workspace-project-targets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ describe('new workspace project targets', () => {
})
})

it('shows one target per host when the project has multiple local worktrees', () => {
it('keeps each same-host clone selectable with its concrete target', () => {
const upstream = { owner: 'stablyai', repo: 'orca' }
const repos = [
{ id: 'local-a', displayName: 'orca-a', path: '/src/orca-a', upstream },
Expand All @@ -88,7 +88,10 @@ describe('new workspace project targets', () => {
upstream
}
]
const projectId = buildNewWorkspaceProjectOptions(repos)[0]?.id ?? null
const projects = buildNewWorkspaceProjectOptions(repos)
expect(projects.map((project) => project.id)).toEqual(['github:stablyai/orca', 'repo:local-b'])
expect(buildNewWorkspaceRunTargetOptions(repos, 'repo:local-b')[0]?.repo).toBe(repos[1])
const projectId = projects[0]?.id ?? null

expect(buildNewWorkspaceRunTargetOptions(repos, projectId, 'darwin')).toEqual([
expect.objectContaining({ id: 'local', label: LOCAL_HOST_LABEL, detail: '/src/orca-a' }),
Expand All @@ -99,4 +102,13 @@ describe('new workspace project targets', () => {
})
])
})
it('uses the complete claim catalog before hiding ineligible create targets', () => {
const upstream = { owner: 'acme', repo: 'app' }
const repos = [
{ id: 'a', displayName: 'A', path: '', addedAt: 1, upstream },
{ id: 'b', displayName: 'B', path: '/b', addedAt: 2, upstream }
]
expect(buildNewWorkspaceProjectOptions(repos).map((project) => project.id)).toEqual(['repo:b'])
expect(buildNewWorkspaceRunTargetOptions(repos, 'repo:b')[0]?.repo).toBe(repos[1])
})
})
Loading
Loading