mirror of
https://github.com/stablyai/orca.git
synced 2026-11-03 18:47:40 +00:00
fix(mobile): group by Project with the desktop sidebar's own code (#28850)
* fix(mobile): phone server rows follow the desktop's other-client and sleeping filters A phone created through a desktop now records whom it acted for, so the desktop's Hide other-client workspaces filter keeps the user's own phone workspaces (locally and on servers) and hides only other people's. The desktop relay marks each server row its sidebar would hide, and the rows its own browser or chat tabs keep awake; the phone applies both. * test(e2e): dedupe sidebar row ids in the visibility spec * fix(mobile): address review: owner naming, old-server relayed phones, scope trim * refactor: move the sidebar's project section grouping into shared The project sections the desktop sidebar draws under Group by Project are built from the project/setup projection, the per-host catalog fold, the endpoint host stamp, and the section labelling and ordering rules. Move the pure parts of each into src/shared (no behaviour change) so the paired phone can call the same code instead of guessing the grouping. * fix(mobile): group by Project with the desktop sidebar's own sections The phone grouped Project sections by repo display name, so it merged checkouts the desktop keeps apart (several local stablyai/orca checkouts, or a server's copy) and split what the desktop merges. The phone now reads each host's repo.list, project.list and projectHostSetup.list (the desktop's own and each server's through the relay), folds them with the renderer's shared catalog fold and projection merge, and builds sections with the desktop's shared grouping, labelling and ordering code. Keys, labels and Recent order equal the sidebar's; a host that predates the project lists gets the repo-derived projection, as the desktop does. Header colour and icon come from the section's anchor repo and rows from their own repo by id, as on the desktop. The repo filter lists every catalog repo by id per host instead of merging same-named repos. Group-by labels read Project and None. project.list and projectHostSetup.list are routed for paired phones (read-only, workspace permission). * refactor(mobile): record a phone's owner as creator and fold desktop tab activity into the host flag A phone now records its owner as the creator (this host for a directly paired phone, the relaying desktop for a delegated one), so the shared rule is unchanged and onBehalfOf is gone. The relay ORs the desktop's saved browser/chat tabs into hasHostSidebarActivity, so the phone's status dot follows too, and local worktree.ps counts the same signals. The old-server fallback is removed. * test(e2e): poll the phone and the sidebar together in the visibility spec * fix(mobile): one Project-section builder for desktop and phone, with the desktop's repo order Review fixes. The sidebar's buildOrderedGroups now calls the shared buildProjectSections for Group by Project (notice and pending repos are optional inputs), so desktop and phone run one function. The phone ranks manual header order by its folded catalog order, which is each host's repo.list order with the desktop's own repos first, as the renderer loads them; only the desktop's cross-host manualRepoOrder (pairing-local) is unknown to it. The projection selector is now a memo over the shared resolver. A transient project-list failure keeps that host's last catalog; only forbidden or method_not_found falls back to the repo-derived projection. Catalog rows are checked on every member grouping reads, and the parity test answers the renderer's real catalog fetch, includes a server's own project rows, and fixes Manual and Recent orders that differ. * fix(mobile): use the repo-derived catalog until a host's project lists first answer * fix(mobile): the desktop serves its sidebar's Project catalog instead of the phone fetching it The phone fetched repo.list, project.list and projectHostSetup.list from the desktop and every relay-ready server each minute and folded them itself, so rows from offline or older servers fell back to repo sections and keys moved on cold start. The desktop renderer now publishes the inputs its own Project headers use (state.repos in sidebar order, the projection selector's projects and setups, projectOrderBy) on the runtime window graph it already sends to main, only when they change. Main keeps the last one and answers it on a new paired-desktop read, mobileRelay.projectSections. The phone reads it with its repo metadata and groups with the shared builder; a desktop that predates the read (forbidden or method_not_found) leaves the phone grouping rows by repo from the desktop's repo.list, and a transient failure keeps the last catalog. The payload carries only grouping and label fields, no hooks, credentials or uploaded icons. The phone's own catalog fetching and its project.list/projectHostSetup.list routes are removed. * fix(mobile): keep uploaded repo icons on the phone, and isolate the snapshot tests * test(e2e): the desktop window serves its sidebar's Project catalog to the phone * chore(i18n): regenerate the runtime English catalog (drift already on main) * fix(mobile): read the Project catalog as its own request, outside the recorded repo-metadata flow The extra read inside the repo-metadata refresh stalled its recorded RPC sequence. It now runs beside it with its own throttle. The repo-metadata goldens are re-recorded for the intended state change: the name-keyed colour, icon and id maps became desktopRepos. * fix(mobile): fetch the Project catalog at once when the screen moves to another host The refresh window is now kept per client, so a host screen reused for another desktop doesn't wait out the previous host's 60s window before showing its Project headers.
This commit is contained in:
@@ -10074,7 +10074,7 @@
|
||||
"src/renderer/src/lib/renderer-agent-status-observation-ingress.test.ts": 137,
|
||||
"src/renderer/src/lib/renderer-app-platform.test.ts": 177,
|
||||
"src/renderer/src/lib/renderer-memory-profile.test.ts": 64,
|
||||
"src/renderer/src/lib/repo-display-labels.test.ts": 50,
|
||||
"src/shared/repo-display-labels.test.ts": 50,
|
||||
"src/renderer/src/lib/repo-runtime-owner.test.ts": 61,
|
||||
"src/renderer/src/lib/repo-search.test.ts": 43,
|
||||
"src/renderer/src/lib/repo-slug-cache.test.ts": 37,
|
||||
|
||||
+482
-494
File diff suppressed because it is too large
Load Diff
@@ -4,80 +4,6 @@
|
||||
"family": "settings.repo-metadata",
|
||||
"namedDeltas": [],
|
||||
"values": {
|
||||
"03c9a2ba60b9": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 9,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "host.platform"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-4",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"platform": "linux"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"0c4296f215a4": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 8,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "settings.get"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-3",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"settings": {
|
||||
"defaultTuiAgent": "codex",
|
||||
"disabledTuiAgents": ["claude"],
|
||||
"hostSettingOverrides": {},
|
||||
"prBotAuthorOverrides": ["bot-user"],
|
||||
"visibleTaskProviders": ["github", "linear"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"12e70439f294": {
|
||||
"name": "repo.list#1",
|
||||
"ordinal": 1,
|
||||
@@ -145,18 +71,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"19b2979d7fc2": {
|
||||
"name": "repo.list#1",
|
||||
"ordinal": 2,
|
||||
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.list\"}"
|
||||
},
|
||||
"1a160fc2d137": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 9,
|
||||
"18762ff064ed": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 6,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "host.platform"
|
||||
"value": "settings.get"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
@@ -172,10 +93,29 @@
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-3",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"settings": {
|
||||
"defaultTuiAgent": "codex",
|
||||
"disabledTuiAgents": ["claude"],
|
||||
"hostSettingOverrides": {},
|
||||
"prBotAuthorOverrides": ["bot-user"],
|
||||
"visibleTaskProviders": ["github", "linear"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"19b2979d7fc2": {
|
||||
"name": "repo.list#1",
|
||||
"ordinal": 2,
|
||||
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.list\"}"
|
||||
},
|
||||
"1b23dab83fcf": {
|
||||
"name": "repo.list#1",
|
||||
"ordinal": 1,
|
||||
@@ -210,23 +150,44 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"1ff13ee8621c": {
|
||||
"name": "repoIconsByName",
|
||||
"ordinal": 4,
|
||||
"value": []
|
||||
},
|
||||
"2f1b95eac8f1": {
|
||||
"name": "repoColorsByName",
|
||||
"ordinal": 3,
|
||||
"value": [
|
||||
["Local", "#6366f1"],
|
||||
["Remote", "#f97316"]
|
||||
]
|
||||
},
|
||||
"36a96ddbd396": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 12,
|
||||
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"host.platform\"}"
|
||||
"299d571abf13": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 5,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "ssh.listTargetSummaries"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-2",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"targets": [
|
||||
{
|
||||
"id": "ssh-1",
|
||||
"label": "SSH"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"44136fa355b3": {},
|
||||
"4526bff18b74": {
|
||||
@@ -310,30 +271,39 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"53226cb82ec3": {
|
||||
"name": "hostPlatform",
|
||||
"ordinal": 15,
|
||||
"value": "linux"
|
||||
},
|
||||
"6063e534d9db": {
|
||||
"name": "hostHealthById",
|
||||
"ordinal": 13,
|
||||
"value": []
|
||||
},
|
||||
"6134b73f18d0": {
|
||||
"repoColorsByName": [
|
||||
["Local", "#6366f1"],
|
||||
["Remote", "#f97316"]
|
||||
"5248b37ec085": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 7,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "host.platform"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"repoHostIdByRepoId": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
],
|
||||
"repoIconsByName": [],
|
||||
"repoIdsByName": [
|
||||
["Local", "repo-1"],
|
||||
["Remote", "repo-2"]
|
||||
]
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-4",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"platform": "linux"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"624ec4e5082c": {
|
||||
"name": "repo.list#1",
|
||||
@@ -370,10 +340,66 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"668a7e313975": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 10,
|
||||
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.listTargetSummaries\"}"
|
||||
"661b4721aec6": {
|
||||
"name": "desktopRepos",
|
||||
"ordinal": 3,
|
||||
"value": [
|
||||
{
|
||||
"displayName": "Local",
|
||||
"executionHostId": "local",
|
||||
"id": "repo-1",
|
||||
"path": "repo-1"
|
||||
},
|
||||
{
|
||||
"displayName": "Remote",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-2",
|
||||
"path": "repo-2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"68916f125c3c": {
|
||||
"desktopRepos": [
|
||||
{
|
||||
"displayName": "Local",
|
||||
"executionHostId": "local",
|
||||
"id": "repo-1",
|
||||
"path": "repo-1"
|
||||
},
|
||||
{
|
||||
"displayName": "Remote",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-2",
|
||||
"path": "repo-2"
|
||||
}
|
||||
],
|
||||
"repoHostIdByRepoId": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
]
|
||||
},
|
||||
"6d43117882f3": {
|
||||
"desktopRepos": [
|
||||
{
|
||||
"displayName": "Local",
|
||||
"executionHostId": "local",
|
||||
"id": "repo-1",
|
||||
"path": "repo-1"
|
||||
},
|
||||
{
|
||||
"displayName": "Remote",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-2",
|
||||
"path": "repo-2"
|
||||
}
|
||||
],
|
||||
"hostHealthById": [],
|
||||
"hostLabelById": [["ssh:ssh-1", "SSH"]],
|
||||
"hostPlatform": "linux",
|
||||
"repoHostIdByRepoId": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
]
|
||||
},
|
||||
"747c556da67a": {
|
||||
"name": "repo.list#1",
|
||||
@@ -406,13 +432,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"7e772c1c111d": {
|
||||
"name": "repoIdsByName",
|
||||
"ordinal": 5,
|
||||
"value": [
|
||||
["Local", "repo-1"],
|
||||
["Remote", "repo-2"]
|
||||
]
|
||||
"7c508088602c": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 7,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "host.platform"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
}
|
||||
},
|
||||
"8a72d3d14d44": {
|
||||
"name": "repo.list#1",
|
||||
@@ -449,21 +493,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"8ef3b348c179": {
|
||||
"name": "repoHostIdByRepoId",
|
||||
"ordinal": 6,
|
||||
"value": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
]
|
||||
},
|
||||
"9270aeb7d9c6": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
},
|
||||
"96e0c54d86b0": {
|
||||
"8b618db22116": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}"
|
||||
},
|
||||
"902c0b2ed3ce": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 6,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
@@ -487,54 +524,24 @@
|
||||
"startedAt": 0
|
||||
}
|
||||
},
|
||||
"b428bce85ee7": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 11,
|
||||
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}"
|
||||
"91e7f4ef9a77": {
|
||||
"name": "hostPlatform",
|
||||
"ordinal": 13,
|
||||
"value": "linux"
|
||||
},
|
||||
"bea13a4a2e87": {
|
||||
"name": "hostLabelById",
|
||||
"ordinal": 14,
|
||||
"value": [["ssh:ssh-1", "SSH"]]
|
||||
"9270aeb7d9c6": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
},
|
||||
"c752e67787f9": {
|
||||
"c1246f5792ee": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 7,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "ssh.listTargetSummaries"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-2",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"targets": [
|
||||
{
|
||||
"id": "ssh-1",
|
||||
"label": "SSH"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
"ordinal": 8,
|
||||
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.listTargetSummaries\"}"
|
||||
},
|
||||
"c1d6fc758acc": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 10,
|
||||
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"host.platform\"}"
|
||||
},
|
||||
"d68475063b62": {
|
||||
"name": "repo.list#1",
|
||||
@@ -573,6 +580,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"d9c464d0dc78": {
|
||||
"name": "hostLabelById",
|
||||
"ordinal": 12,
|
||||
"value": [["ssh:ssh-1", "SSH"]]
|
||||
},
|
||||
"dae756300589": {
|
||||
"name": "repo.list#1",
|
||||
"ordinal": 1,
|
||||
@@ -605,6 +617,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"e2881f748a46": {
|
||||
"name": "hostHealthById",
|
||||
"ordinal": 11,
|
||||
"value": []
|
||||
},
|
||||
"e49da3e1abca": {
|
||||
"name": "repoHostIdByRepoId",
|
||||
"ordinal": 4,
|
||||
"value": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
]
|
||||
},
|
||||
"eb79a9b3682a": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
@@ -647,24 +672,6 @@
|
||||
"ok": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"f4667dda0187": {
|
||||
"hostHealthById": [],
|
||||
"hostLabelById": [["ssh:ssh-1", "SSH"]],
|
||||
"hostPlatform": "linux",
|
||||
"repoColorsByName": [
|
||||
["Local", "#6366f1"],
|
||||
["Remote", "#f97316"]
|
||||
],
|
||||
"repoHostIdByRepoId": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
],
|
||||
"repoIconsByName": [],
|
||||
"repoIdsByName": [
|
||||
["Local", "repo-1"],
|
||||
["Remote", "repo-2"]
|
||||
]
|
||||
}
|
||||
},
|
||||
"recording": {
|
||||
@@ -673,34 +680,32 @@
|
||||
{
|
||||
"id": "settings-repo-metadata-fulfilled.normal:settings-pending",
|
||||
"observation": {
|
||||
"sender": ["4526bff18b74", "c752e67787f9", "96e0c54d86b0", "1a160fc2d137"],
|
||||
"payloads": ["19b2979d7fc2", "668a7e313975", "b428bce85ee7", "36a96ddbd396"],
|
||||
"sender": ["4526bff18b74", "299d571abf13", "902c0b2ed3ce", "7c508088602c"],
|
||||
"payloads": ["19b2979d7fc2", "c1246f5792ee", "8b618db22116", "c1d6fc758acc"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"load": "9270aeb7d9c6"
|
||||
},
|
||||
"state": "6134b73f18d0",
|
||||
"effects": ["2f1b95eac8f1", "1ff13ee8621c", "7e772c1c111d", "8ef3b348c179"]
|
||||
"state": "68916f125c3c",
|
||||
"effects": ["661b4721aec6", "e49da3e1abca"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "settings-repo-metadata-fulfilled.normal:settled",
|
||||
"observation": {
|
||||
"sender": ["4526bff18b74", "c752e67787f9", "0c4296f215a4", "03c9a2ba60b9"],
|
||||
"payloads": ["19b2979d7fc2", "668a7e313975", "b428bce85ee7", "36a96ddbd396"],
|
||||
"sender": ["4526bff18b74", "299d571abf13", "18762ff064ed", "5248b37ec085"],
|
||||
"payloads": ["19b2979d7fc2", "c1246f5792ee", "8b618db22116", "c1d6fc758acc"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"load": "eb79a9b3682a"
|
||||
},
|
||||
"state": "f4667dda0187",
|
||||
"state": "6d43117882f3",
|
||||
"effects": [
|
||||
"2f1b95eac8f1",
|
||||
"1ff13ee8621c",
|
||||
"7e772c1c111d",
|
||||
"8ef3b348c179",
|
||||
"6063e534d9db",
|
||||
"bea13a4a2e87",
|
||||
"53226cb82ec3"
|
||||
"661b4721aec6",
|
||||
"e49da3e1abca",
|
||||
"e2881f748a46",
|
||||
"d9c464d0dc78",
|
||||
"91e7f4ef9a77"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+471
-481
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -4,43 +4,9 @@
|
||||
"family": "settings.repo-metadata",
|
||||
"namedDeltas": [],
|
||||
"values": {
|
||||
"03c9a2ba60b9": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 9,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "host.platform"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-4",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"platform": "linux"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"0c4296f215a4": {
|
||||
"18762ff064ed": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 8,
|
||||
"ordinal": 6,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
@@ -83,13 +49,13 @@
|
||||
"ordinal": 2,
|
||||
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.list\"}"
|
||||
},
|
||||
"1a160fc2d137": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 9,
|
||||
"299d571abf13": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 5,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "host.platform"
|
||||
"value": "ssh.listTargetSummaries"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
@@ -105,33 +71,28 @@
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-2",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"targets": [
|
||||
{
|
||||
"id": "ssh-1",
|
||||
"label": "SSH"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"1ff13ee8621c": {
|
||||
"name": "repoIconsByName",
|
||||
"ordinal": 4,
|
||||
"value": []
|
||||
},
|
||||
"2053629911e7": {
|
||||
"35f30397fcb2": {
|
||||
"name": "repo.list#2",
|
||||
"ordinal": 17,
|
||||
"ordinal": 15,
|
||||
"json": "{\"id\":\"frame-5\",\"deviceToken\":\"recording-device\",\"method\":\"repo.list\"}"
|
||||
},
|
||||
"2f1b95eac8f1": {
|
||||
"name": "repoColorsByName",
|
||||
"ordinal": 3,
|
||||
"value": [
|
||||
["Local", "#6366f1"],
|
||||
["Remote", "#f97316"]
|
||||
]
|
||||
},
|
||||
"36a96ddbd396": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 12,
|
||||
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"host.platform\"}"
|
||||
},
|
||||
"4526bff18b74": {
|
||||
"name": "repo.list#1",
|
||||
"ordinal": 1,
|
||||
@@ -179,59 +140,135 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"53226cb82ec3": {
|
||||
"name": "hostPlatform",
|
||||
"ordinal": 15,
|
||||
"value": "linux"
|
||||
"5248b37ec085": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 7,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "host.platform"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-4",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"platform": "linux"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"6063e534d9db": {
|
||||
"name": "hostHealthById",
|
||||
"ordinal": 13,
|
||||
"value": []
|
||||
"661b4721aec6": {
|
||||
"name": "desktopRepos",
|
||||
"ordinal": 3,
|
||||
"value": [
|
||||
{
|
||||
"displayName": "Local",
|
||||
"executionHostId": "local",
|
||||
"id": "repo-1",
|
||||
"path": "repo-1"
|
||||
},
|
||||
{
|
||||
"displayName": "Remote",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-2",
|
||||
"path": "repo-2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"6134b73f18d0": {
|
||||
"repoColorsByName": [
|
||||
["Local", "#6366f1"],
|
||||
["Remote", "#f97316"]
|
||||
"68916f125c3c": {
|
||||
"desktopRepos": [
|
||||
{
|
||||
"displayName": "Local",
|
||||
"executionHostId": "local",
|
||||
"id": "repo-1",
|
||||
"path": "repo-1"
|
||||
},
|
||||
{
|
||||
"displayName": "Remote",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-2",
|
||||
"path": "repo-2"
|
||||
}
|
||||
],
|
||||
"repoHostIdByRepoId": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
]
|
||||
},
|
||||
"6d43117882f3": {
|
||||
"desktopRepos": [
|
||||
{
|
||||
"displayName": "Local",
|
||||
"executionHostId": "local",
|
||||
"id": "repo-1",
|
||||
"path": "repo-1"
|
||||
},
|
||||
{
|
||||
"displayName": "Remote",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-2",
|
||||
"path": "repo-2"
|
||||
}
|
||||
],
|
||||
"repoIconsByName": [],
|
||||
"repoIdsByName": [
|
||||
["Local", "repo-1"],
|
||||
["Remote", "repo-2"]
|
||||
]
|
||||
},
|
||||
"668a7e313975": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 10,
|
||||
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.listTargetSummaries\"}"
|
||||
},
|
||||
"7e772c1c111d": {
|
||||
"name": "repoIdsByName",
|
||||
"ordinal": 5,
|
||||
"value": [
|
||||
["Local", "repo-1"],
|
||||
["Remote", "repo-2"]
|
||||
]
|
||||
},
|
||||
"8ef3b348c179": {
|
||||
"name": "repoHostIdByRepoId",
|
||||
"ordinal": 6,
|
||||
"value": [
|
||||
"hostHealthById": [],
|
||||
"hostLabelById": [["ssh:ssh-1", "SSH"]],
|
||||
"hostPlatform": "linux",
|
||||
"repoHostIdByRepoId": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
]
|
||||
},
|
||||
"9270aeb7d9c6": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
"7c508088602c": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 7,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "host.platform"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
}
|
||||
},
|
||||
"96e0c54d86b0": {
|
||||
"8b618db22116": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}"
|
||||
},
|
||||
"902c0b2ed3ce": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 6,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
@@ -255,23 +292,9 @@
|
||||
"startedAt": 0
|
||||
}
|
||||
},
|
||||
"b428bce85ee7": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 11,
|
||||
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}"
|
||||
},
|
||||
"ba8a19e3e2b1": {
|
||||
"status": "pending",
|
||||
"startedAt": 60000
|
||||
},
|
||||
"bea13a4a2e87": {
|
||||
"name": "hostLabelById",
|
||||
"ordinal": 14,
|
||||
"value": [["ssh:ssh-1", "SSH"]]
|
||||
},
|
||||
"c402c05b8b96": {
|
||||
"9157a9ad3cbd": {
|
||||
"name": "repo.list#2",
|
||||
"ordinal": 16,
|
||||
"ordinal": 14,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
@@ -295,44 +318,46 @@
|
||||
"startedAt": 60000
|
||||
}
|
||||
},
|
||||
"c752e67787f9": {
|
||||
"91e7f4ef9a77": {
|
||||
"name": "hostPlatform",
|
||||
"ordinal": 13,
|
||||
"value": "linux"
|
||||
},
|
||||
"9270aeb7d9c6": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
},
|
||||
"ba8a19e3e2b1": {
|
||||
"status": "pending",
|
||||
"startedAt": 60000
|
||||
},
|
||||
"c1246f5792ee": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 7,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "ssh.listTargetSummaries"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-2",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"targets": [
|
||||
{
|
||||
"id": "ssh-1",
|
||||
"label": "SSH"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
"ordinal": 8,
|
||||
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.listTargetSummaries\"}"
|
||||
},
|
||||
"c1d6fc758acc": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 10,
|
||||
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"host.platform\"}"
|
||||
},
|
||||
"d9c464d0dc78": {
|
||||
"name": "hostLabelById",
|
||||
"ordinal": 12,
|
||||
"value": [["ssh:ssh-1", "SSH"]]
|
||||
},
|
||||
"e2881f748a46": {
|
||||
"name": "hostHealthById",
|
||||
"ordinal": 11,
|
||||
"value": []
|
||||
},
|
||||
"e49da3e1abca": {
|
||||
"name": "repoHostIdByRepoId",
|
||||
"ordinal": 4,
|
||||
"value": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
]
|
||||
},
|
||||
"eb79a9b3682a": {
|
||||
"status": "fulfilled",
|
||||
@@ -349,24 +374,6 @@
|
||||
"value": {
|
||||
"$rpc": "undefined"
|
||||
}
|
||||
},
|
||||
"f4667dda0187": {
|
||||
"hostHealthById": [],
|
||||
"hostLabelById": [["ssh:ssh-1", "SSH"]],
|
||||
"hostPlatform": "linux",
|
||||
"repoColorsByName": [
|
||||
["Local", "#6366f1"],
|
||||
["Remote", "#f97316"]
|
||||
],
|
||||
"repoHostIdByRepoId": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
],
|
||||
"repoIconsByName": [],
|
||||
"repoIdsByName": [
|
||||
["Local", "repo-1"],
|
||||
["Remote", "repo-2"]
|
||||
]
|
||||
}
|
||||
},
|
||||
"recording": {
|
||||
@@ -375,79 +382,73 @@
|
||||
{
|
||||
"id": "settings-pending",
|
||||
"observation": {
|
||||
"sender": ["4526bff18b74", "c752e67787f9", "96e0c54d86b0", "1a160fc2d137"],
|
||||
"payloads": ["19b2979d7fc2", "668a7e313975", "b428bce85ee7", "36a96ddbd396"],
|
||||
"sender": ["4526bff18b74", "299d571abf13", "902c0b2ed3ce", "7c508088602c"],
|
||||
"payloads": ["19b2979d7fc2", "c1246f5792ee", "8b618db22116", "c1d6fc758acc"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"load": "9270aeb7d9c6"
|
||||
},
|
||||
"state": "6134b73f18d0",
|
||||
"effects": ["2f1b95eac8f1", "1ff13ee8621c", "7e772c1c111d", "8ef3b348c179"]
|
||||
"state": "68916f125c3c",
|
||||
"effects": ["661b4721aec6", "e49da3e1abca"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "settled",
|
||||
"observation": {
|
||||
"sender": ["4526bff18b74", "c752e67787f9", "0c4296f215a4", "03c9a2ba60b9"],
|
||||
"payloads": ["19b2979d7fc2", "668a7e313975", "b428bce85ee7", "36a96ddbd396"],
|
||||
"sender": ["4526bff18b74", "299d571abf13", "18762ff064ed", "5248b37ec085"],
|
||||
"payloads": ["19b2979d7fc2", "c1246f5792ee", "8b618db22116", "c1d6fc758acc"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"load": "eb79a9b3682a"
|
||||
},
|
||||
"state": "f4667dda0187",
|
||||
"state": "6d43117882f3",
|
||||
"effects": [
|
||||
"2f1b95eac8f1",
|
||||
"1ff13ee8621c",
|
||||
"7e772c1c111d",
|
||||
"8ef3b348c179",
|
||||
"6063e534d9db",
|
||||
"bea13a4a2e87",
|
||||
"53226cb82ec3"
|
||||
"661b4721aec6",
|
||||
"e49da3e1abca",
|
||||
"e2881f748a46",
|
||||
"d9c464d0dc78",
|
||||
"91e7f4ef9a77"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "cache-hit",
|
||||
"observation": {
|
||||
"sender": ["4526bff18b74", "c752e67787f9", "0c4296f215a4", "03c9a2ba60b9"],
|
||||
"payloads": ["19b2979d7fc2", "668a7e313975", "b428bce85ee7", "36a96ddbd396"],
|
||||
"sender": ["4526bff18b74", "299d571abf13", "18762ff064ed", "5248b37ec085"],
|
||||
"payloads": ["19b2979d7fc2", "c1246f5792ee", "8b618db22116", "c1d6fc758acc"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"load": "eb79a9b3682a",
|
||||
"cached-query": "eb79a9b3682a"
|
||||
},
|
||||
"state": "f4667dda0187",
|
||||
"state": "6d43117882f3",
|
||||
"effects": [
|
||||
"2f1b95eac8f1",
|
||||
"1ff13ee8621c",
|
||||
"7e772c1c111d",
|
||||
"8ef3b348c179",
|
||||
"6063e534d9db",
|
||||
"bea13a4a2e87",
|
||||
"53226cb82ec3"
|
||||
"661b4721aec6",
|
||||
"e49da3e1abca",
|
||||
"e2881f748a46",
|
||||
"d9c464d0dc78",
|
||||
"91e7f4ef9a77"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "cache-warm",
|
||||
"observation": {
|
||||
"sender": ["4526bff18b74", "c752e67787f9", "0c4296f215a4", "03c9a2ba60b9"],
|
||||
"payloads": ["19b2979d7fc2", "668a7e313975", "b428bce85ee7", "36a96ddbd396"],
|
||||
"sender": ["4526bff18b74", "299d571abf13", "18762ff064ed", "5248b37ec085"],
|
||||
"payloads": ["19b2979d7fc2", "c1246f5792ee", "8b618db22116", "c1d6fc758acc"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"load": "eb79a9b3682a",
|
||||
"cached-query": "eb79a9b3682a",
|
||||
"warm-query": "f43afee17848"
|
||||
},
|
||||
"state": "f4667dda0187",
|
||||
"state": "6d43117882f3",
|
||||
"effects": [
|
||||
"2f1b95eac8f1",
|
||||
"1ff13ee8621c",
|
||||
"7e772c1c111d",
|
||||
"8ef3b348c179",
|
||||
"6063e534d9db",
|
||||
"bea13a4a2e87",
|
||||
"53226cb82ec3"
|
||||
"661b4721aec6",
|
||||
"e49da3e1abca",
|
||||
"e2881f748a46",
|
||||
"d9c464d0dc78",
|
||||
"91e7f4ef9a77"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -456,17 +457,17 @@
|
||||
"observation": {
|
||||
"sender": [
|
||||
"4526bff18b74",
|
||||
"c752e67787f9",
|
||||
"0c4296f215a4",
|
||||
"03c9a2ba60b9",
|
||||
"c402c05b8b96"
|
||||
"299d571abf13",
|
||||
"18762ff064ed",
|
||||
"5248b37ec085",
|
||||
"9157a9ad3cbd"
|
||||
],
|
||||
"payloads": [
|
||||
"19b2979d7fc2",
|
||||
"668a7e313975",
|
||||
"b428bce85ee7",
|
||||
"36a96ddbd396",
|
||||
"2053629911e7"
|
||||
"c1246f5792ee",
|
||||
"8b618db22116",
|
||||
"c1d6fc758acc",
|
||||
"35f30397fcb2"
|
||||
],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
@@ -475,15 +476,13 @@
|
||||
"warm-query": "f43afee17848",
|
||||
"expired-query": "ba8a19e3e2b1"
|
||||
},
|
||||
"state": "f4667dda0187",
|
||||
"state": "6d43117882f3",
|
||||
"effects": [
|
||||
"2f1b95eac8f1",
|
||||
"1ff13ee8621c",
|
||||
"7e772c1c111d",
|
||||
"8ef3b348c179",
|
||||
"6063e534d9db",
|
||||
"bea13a4a2e87",
|
||||
"53226cb82ec3"
|
||||
"661b4721aec6",
|
||||
"e49da3e1abca",
|
||||
"e2881f748a46",
|
||||
"d9c464d0dc78",
|
||||
"91e7f4ef9a77"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,43 +4,9 @@
|
||||
"family": "settings.repo-metadata",
|
||||
"namedDeltas": [],
|
||||
"values": {
|
||||
"03c9a2ba60b9": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 9,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "host.platform"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-4",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"platform": "linux"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"0c4296f215a4": {
|
||||
"18762ff064ed": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 8,
|
||||
"ordinal": 6,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
@@ -83,13 +49,13 @@
|
||||
"ordinal": 2,
|
||||
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.list\"}"
|
||||
},
|
||||
"1a160fc2d137": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 9,
|
||||
"299d571abf13": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 5,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "host.platform"
|
||||
"value": "ssh.listTargetSummaries"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
@@ -105,28 +71,23 @@
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-2",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"targets": [
|
||||
{
|
||||
"id": "ssh-1",
|
||||
"label": "SSH"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"1ff13ee8621c": {
|
||||
"name": "repoIconsByName",
|
||||
"ordinal": 4,
|
||||
"value": []
|
||||
},
|
||||
"2f1b95eac8f1": {
|
||||
"name": "repoColorsByName",
|
||||
"ordinal": 3,
|
||||
"value": [
|
||||
["Local", "#6366f1"],
|
||||
["Remote", "#f97316"]
|
||||
]
|
||||
},
|
||||
"36a96ddbd396": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 12,
|
||||
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"host.platform\"}"
|
||||
},
|
||||
"4526bff18b74": {
|
||||
"name": "repo.list#1",
|
||||
"ordinal": 1,
|
||||
@@ -174,59 +135,135 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"53226cb82ec3": {
|
||||
"name": "hostPlatform",
|
||||
"ordinal": 15,
|
||||
"value": "linux"
|
||||
"5248b37ec085": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 7,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "host.platform"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-4",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"platform": "linux"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"6063e534d9db": {
|
||||
"name": "hostHealthById",
|
||||
"ordinal": 13,
|
||||
"value": []
|
||||
"661b4721aec6": {
|
||||
"name": "desktopRepos",
|
||||
"ordinal": 3,
|
||||
"value": [
|
||||
{
|
||||
"displayName": "Local",
|
||||
"executionHostId": "local",
|
||||
"id": "repo-1",
|
||||
"path": "repo-1"
|
||||
},
|
||||
{
|
||||
"displayName": "Remote",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-2",
|
||||
"path": "repo-2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"6134b73f18d0": {
|
||||
"repoColorsByName": [
|
||||
["Local", "#6366f1"],
|
||||
["Remote", "#f97316"]
|
||||
"68916f125c3c": {
|
||||
"desktopRepos": [
|
||||
{
|
||||
"displayName": "Local",
|
||||
"executionHostId": "local",
|
||||
"id": "repo-1",
|
||||
"path": "repo-1"
|
||||
},
|
||||
{
|
||||
"displayName": "Remote",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-2",
|
||||
"path": "repo-2"
|
||||
}
|
||||
],
|
||||
"repoHostIdByRepoId": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
]
|
||||
},
|
||||
"6d43117882f3": {
|
||||
"desktopRepos": [
|
||||
{
|
||||
"displayName": "Local",
|
||||
"executionHostId": "local",
|
||||
"id": "repo-1",
|
||||
"path": "repo-1"
|
||||
},
|
||||
{
|
||||
"displayName": "Remote",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-2",
|
||||
"path": "repo-2"
|
||||
}
|
||||
],
|
||||
"repoIconsByName": [],
|
||||
"repoIdsByName": [
|
||||
["Local", "repo-1"],
|
||||
["Remote", "repo-2"]
|
||||
]
|
||||
},
|
||||
"668a7e313975": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 10,
|
||||
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.listTargetSummaries\"}"
|
||||
},
|
||||
"7e772c1c111d": {
|
||||
"name": "repoIdsByName",
|
||||
"ordinal": 5,
|
||||
"value": [
|
||||
["Local", "repo-1"],
|
||||
["Remote", "repo-2"]
|
||||
]
|
||||
},
|
||||
"8ef3b348c179": {
|
||||
"name": "repoHostIdByRepoId",
|
||||
"ordinal": 6,
|
||||
"value": [
|
||||
"hostHealthById": [],
|
||||
"hostLabelById": [["ssh:ssh-1", "SSH"]],
|
||||
"hostPlatform": "linux",
|
||||
"repoHostIdByRepoId": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
]
|
||||
},
|
||||
"9270aeb7d9c6": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
"7c508088602c": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 7,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "host.platform"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
}
|
||||
},
|
||||
"96e0c54d86b0": {
|
||||
"8b618db22116": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}"
|
||||
},
|
||||
"902c0b2ed3ce": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 6,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
@@ -250,54 +287,42 @@
|
||||
"startedAt": 0
|
||||
}
|
||||
},
|
||||
"b428bce85ee7": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 11,
|
||||
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}"
|
||||
"91e7f4ef9a77": {
|
||||
"name": "hostPlatform",
|
||||
"ordinal": 13,
|
||||
"value": "linux"
|
||||
},
|
||||
"bea13a4a2e87": {
|
||||
"9270aeb7d9c6": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
},
|
||||
"c1246f5792ee": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 8,
|
||||
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.listTargetSummaries\"}"
|
||||
},
|
||||
"c1d6fc758acc": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 10,
|
||||
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"host.platform\"}"
|
||||
},
|
||||
"d9c464d0dc78": {
|
||||
"name": "hostLabelById",
|
||||
"ordinal": 14,
|
||||
"ordinal": 12,
|
||||
"value": [["ssh:ssh-1", "SSH"]]
|
||||
},
|
||||
"c752e67787f9": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 7,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "ssh.listTargetSummaries"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-2",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"targets": [
|
||||
{
|
||||
"id": "ssh-1",
|
||||
"label": "SSH"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
"e2881f748a46": {
|
||||
"name": "hostHealthById",
|
||||
"ordinal": 11,
|
||||
"value": []
|
||||
},
|
||||
"e49da3e1abca": {
|
||||
"name": "repoHostIdByRepoId",
|
||||
"ordinal": 4,
|
||||
"value": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
]
|
||||
},
|
||||
"eb79a9b3682a": {
|
||||
"status": "fulfilled",
|
||||
@@ -306,24 +331,6 @@
|
||||
"value": {
|
||||
"$rpc": "undefined"
|
||||
}
|
||||
},
|
||||
"f4667dda0187": {
|
||||
"hostHealthById": [],
|
||||
"hostLabelById": [["ssh:ssh-1", "SSH"]],
|
||||
"hostPlatform": "linux",
|
||||
"repoColorsByName": [
|
||||
["Local", "#6366f1"],
|
||||
["Remote", "#f97316"]
|
||||
],
|
||||
"repoHostIdByRepoId": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
],
|
||||
"repoIconsByName": [],
|
||||
"repoIdsByName": [
|
||||
["Local", "repo-1"],
|
||||
["Remote", "repo-2"]
|
||||
]
|
||||
}
|
||||
},
|
||||
"recording": {
|
||||
@@ -332,34 +339,32 @@
|
||||
{
|
||||
"id": "settings-pending",
|
||||
"observation": {
|
||||
"sender": ["4526bff18b74", "c752e67787f9", "96e0c54d86b0", "1a160fc2d137"],
|
||||
"payloads": ["19b2979d7fc2", "668a7e313975", "b428bce85ee7", "36a96ddbd396"],
|
||||
"sender": ["4526bff18b74", "299d571abf13", "902c0b2ed3ce", "7c508088602c"],
|
||||
"payloads": ["19b2979d7fc2", "c1246f5792ee", "8b618db22116", "c1d6fc758acc"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"load": "9270aeb7d9c6"
|
||||
},
|
||||
"state": "6134b73f18d0",
|
||||
"effects": ["2f1b95eac8f1", "1ff13ee8621c", "7e772c1c111d", "8ef3b348c179"]
|
||||
"state": "68916f125c3c",
|
||||
"effects": ["661b4721aec6", "e49da3e1abca"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "settled",
|
||||
"observation": {
|
||||
"sender": ["4526bff18b74", "c752e67787f9", "0c4296f215a4", "03c9a2ba60b9"],
|
||||
"payloads": ["19b2979d7fc2", "668a7e313975", "b428bce85ee7", "36a96ddbd396"],
|
||||
"sender": ["4526bff18b74", "299d571abf13", "18762ff064ed", "5248b37ec085"],
|
||||
"payloads": ["19b2979d7fc2", "c1246f5792ee", "8b618db22116", "c1d6fc758acc"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"load": "eb79a9b3682a"
|
||||
},
|
||||
"state": "f4667dda0187",
|
||||
"state": "6d43117882f3",
|
||||
"effects": [
|
||||
"2f1b95eac8f1",
|
||||
"1ff13ee8621c",
|
||||
"7e772c1c111d",
|
||||
"8ef3b348c179",
|
||||
"6063e534d9db",
|
||||
"bea13a4a2e87",
|
||||
"53226cb82ec3"
|
||||
"661b4721aec6",
|
||||
"e49da3e1abca",
|
||||
"e2881f748a46",
|
||||
"d9c464d0dc78",
|
||||
"91e7f4ef9a77"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,27 +4,68 @@
|
||||
"family": "settings.repo-metadata",
|
||||
"namedDeltas": [],
|
||||
"values": {
|
||||
"0fd72e1aba93": {
|
||||
"desktopRepos": [
|
||||
{
|
||||
"badgeColor": "#123456",
|
||||
"displayName": "Lucide",
|
||||
"executionHostId": "local",
|
||||
"id": "repo-1",
|
||||
"path": "repo-1",
|
||||
"repoIcon": {
|
||||
"name": "Rocket",
|
||||
"type": "lucide"
|
||||
}
|
||||
},
|
||||
{
|
||||
"displayName": "Emoji",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-2",
|
||||
"path": "repo-2",
|
||||
"repoIcon": {
|
||||
"emoji": "🐳",
|
||||
"type": "emoji"
|
||||
}
|
||||
},
|
||||
{
|
||||
"displayName": "Avatar",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-3",
|
||||
"path": "repo-3",
|
||||
"repoIcon": {
|
||||
"label": "acme/orca",
|
||||
"source": "github",
|
||||
"src": "https://github.com/acme.png?size=64",
|
||||
"type": "image"
|
||||
}
|
||||
}
|
||||
],
|
||||
"hostHealthById": [],
|
||||
"hostLabelById": [["ssh:ssh-1", "SSH"]],
|
||||
"hostPlatform": "darwin",
|
||||
"repoHostIdByRepoId": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"],
|
||||
["repo-3", "ssh:ssh-1"]
|
||||
]
|
||||
},
|
||||
"19b2979d7fc2": {
|
||||
"name": "repo.list#1",
|
||||
"ordinal": 2,
|
||||
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.list\"}"
|
||||
},
|
||||
"19bf6d2469a6": {
|
||||
"name": "repoColorsByName",
|
||||
"ordinal": 3,
|
||||
"value": [
|
||||
["Lucide", "#123456"],
|
||||
["Emoji", "#f97316"],
|
||||
["Avatar", "#f97316"]
|
||||
]
|
||||
"1c76208c9beb": {
|
||||
"name": "hostPlatform",
|
||||
"ordinal": 13,
|
||||
"value": "darwin"
|
||||
},
|
||||
"1a160fc2d137": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 9,
|
||||
"299d571abf13": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 5,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "host.platform"
|
||||
"value": "ssh.listTargetSummaries"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
@@ -40,70 +81,26 @@
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-2",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"targets": [
|
||||
{
|
||||
"id": "ssh-1",
|
||||
"label": "SSH"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"25b44bca9e45": {
|
||||
"name": "hostPlatform",
|
||||
"ordinal": 15,
|
||||
"value": "darwin"
|
||||
},
|
||||
"36a96ddbd396": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 12,
|
||||
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"host.platform\"}"
|
||||
},
|
||||
"6063e534d9db": {
|
||||
"name": "hostHealthById",
|
||||
"ordinal": 13,
|
||||
"value": []
|
||||
},
|
||||
"6338d57f282f": {
|
||||
"repoColorsByName": [
|
||||
["Lucide", "#123456"],
|
||||
["Emoji", "#f97316"],
|
||||
["Avatar", "#f97316"]
|
||||
],
|
||||
"repoHostIdByRepoId": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"],
|
||||
["repo-3", "ssh:ssh-1"]
|
||||
],
|
||||
"repoIconsByName": [
|
||||
[
|
||||
"Lucide",
|
||||
{
|
||||
"name": "Rocket",
|
||||
"type": "lucide"
|
||||
}
|
||||
],
|
||||
[
|
||||
"Emoji",
|
||||
{
|
||||
"emoji": "🐳",
|
||||
"type": "emoji"
|
||||
}
|
||||
],
|
||||
[
|
||||
"Avatar",
|
||||
{
|
||||
"label": "acme/orca",
|
||||
"source": "github",
|
||||
"src": "https://github.com/acme.png?size=64",
|
||||
"type": "image"
|
||||
}
|
||||
]
|
||||
],
|
||||
"repoIdsByName": [
|
||||
["Lucide", "repo-1"],
|
||||
["Emoji", "repo-2"],
|
||||
["Avatar", "repo-3"]
|
||||
]
|
||||
},
|
||||
"64f5522cc303": {
|
||||
"466c0317a24a": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 8,
|
||||
"ordinal": 6,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
@@ -139,36 +136,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"668a7e313975": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 10,
|
||||
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.listTargetSummaries\"}"
|
||||
"7c508088602c": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 7,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "host.platform"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
}
|
||||
},
|
||||
"6bc5e5b7a259": {
|
||||
"name": "repoHostIdByRepoId",
|
||||
"ordinal": 6,
|
||||
"value": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"],
|
||||
["repo-3", "ssh:ssh-1"]
|
||||
]
|
||||
},
|
||||
"77cbfd1f087c": {
|
||||
"name": "repoIdsByName",
|
||||
"ordinal": 5,
|
||||
"value": [
|
||||
["Lucide", "repo-1"],
|
||||
["Emoji", "repo-2"],
|
||||
["Avatar", "repo-3"]
|
||||
]
|
||||
},
|
||||
"9270aeb7d9c6": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
},
|
||||
"96e0c54d86b0": {
|
||||
"8b618db22116": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}"
|
||||
},
|
||||
"902c0b2ed3ce": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 6,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
@@ -192,6 +193,86 @@
|
||||
"startedAt": 0
|
||||
}
|
||||
},
|
||||
"910617d262f4": {
|
||||
"desktopRepos": [
|
||||
{
|
||||
"badgeColor": "#123456",
|
||||
"displayName": "Lucide",
|
||||
"executionHostId": "local",
|
||||
"id": "repo-1",
|
||||
"path": "repo-1",
|
||||
"repoIcon": {
|
||||
"name": "Rocket",
|
||||
"type": "lucide"
|
||||
}
|
||||
},
|
||||
{
|
||||
"displayName": "Emoji",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-2",
|
||||
"path": "repo-2",
|
||||
"repoIcon": {
|
||||
"emoji": "🐳",
|
||||
"type": "emoji"
|
||||
}
|
||||
},
|
||||
{
|
||||
"displayName": "Avatar",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-3",
|
||||
"path": "repo-3",
|
||||
"repoIcon": {
|
||||
"label": "acme/orca",
|
||||
"source": "github",
|
||||
"src": "https://github.com/acme.png?size=64",
|
||||
"type": "image"
|
||||
}
|
||||
}
|
||||
],
|
||||
"repoHostIdByRepoId": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"],
|
||||
["repo-3", "ssh:ssh-1"]
|
||||
]
|
||||
},
|
||||
"9270aeb7d9c6": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
},
|
||||
"9bc91d02ce24": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 7,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "host.platform"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-4",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"platform": "darwin"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"a9add325bb0a": {
|
||||
"name": "repo.list#1",
|
||||
"ordinal": 1,
|
||||
@@ -259,128 +340,34 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ae27a5b525fa": {
|
||||
"name": "repoIconsByName",
|
||||
"b4ad28ffe331": {
|
||||
"name": "repoHostIdByRepoId",
|
||||
"ordinal": 4,
|
||||
"value": [
|
||||
[
|
||||
"Lucide",
|
||||
{
|
||||
"name": "Rocket",
|
||||
"type": "lucide"
|
||||
}
|
||||
],
|
||||
[
|
||||
"Emoji",
|
||||
{
|
||||
"emoji": "🐳",
|
||||
"type": "emoji"
|
||||
}
|
||||
],
|
||||
[
|
||||
"Avatar",
|
||||
{
|
||||
"label": "acme/orca",
|
||||
"source": "github",
|
||||
"src": "https://github.com/acme.png?size=64",
|
||||
"type": "image"
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"b428bce85ee7": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 11,
|
||||
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}"
|
||||
},
|
||||
"bea13a4a2e87": {
|
||||
"name": "hostLabelById",
|
||||
"ordinal": 14,
|
||||
"value": [["ssh:ssh-1", "SSH"]]
|
||||
},
|
||||
"c020f709d835": {
|
||||
"hostHealthById": [],
|
||||
"hostLabelById": [["ssh:ssh-1", "SSH"]],
|
||||
"hostPlatform": "darwin",
|
||||
"repoColorsByName": [
|
||||
["Lucide", "#123456"],
|
||||
["Emoji", "#f97316"],
|
||||
["Avatar", "#f97316"]
|
||||
],
|
||||
"repoHostIdByRepoId": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"],
|
||||
["repo-3", "ssh:ssh-1"]
|
||||
],
|
||||
"repoIconsByName": [
|
||||
[
|
||||
"Lucide",
|
||||
{
|
||||
"name": "Rocket",
|
||||
"type": "lucide"
|
||||
}
|
||||
],
|
||||
[
|
||||
"Emoji",
|
||||
{
|
||||
"emoji": "🐳",
|
||||
"type": "emoji"
|
||||
}
|
||||
],
|
||||
[
|
||||
"Avatar",
|
||||
{
|
||||
"label": "acme/orca",
|
||||
"source": "github",
|
||||
"src": "https://github.com/acme.png?size=64",
|
||||
"type": "image"
|
||||
}
|
||||
]
|
||||
],
|
||||
"repoIdsByName": [
|
||||
["Lucide", "repo-1"],
|
||||
["Emoji", "repo-2"],
|
||||
["Avatar", "repo-3"]
|
||||
]
|
||||
},
|
||||
"c752e67787f9": {
|
||||
"c1246f5792ee": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 7,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "ssh.listTargetSummaries"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-2",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"targets": [
|
||||
{
|
||||
"id": "ssh-1",
|
||||
"label": "SSH"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
"ordinal": 8,
|
||||
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.listTargetSummaries\"}"
|
||||
},
|
||||
"c1d6fc758acc": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 10,
|
||||
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"host.platform\"}"
|
||||
},
|
||||
"d9c464d0dc78": {
|
||||
"name": "hostLabelById",
|
||||
"ordinal": 12,
|
||||
"value": [["ssh:ssh-1", "SSH"]]
|
||||
},
|
||||
"e2881f748a46": {
|
||||
"name": "hostHealthById",
|
||||
"ordinal": 11,
|
||||
"value": []
|
||||
},
|
||||
"eb79a9b3682a": {
|
||||
"status": "fulfilled",
|
||||
@@ -390,39 +377,44 @@
|
||||
"$rpc": "undefined"
|
||||
}
|
||||
},
|
||||
"ee8e16df5187": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 9,
|
||||
"args": [
|
||||
"f9217d79f53e": {
|
||||
"name": "desktopRepos",
|
||||
"ordinal": 3,
|
||||
"value": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "host.platform"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
"badgeColor": "#123456",
|
||||
"displayName": "Lucide",
|
||||
"executionHostId": "local",
|
||||
"id": "repo-1",
|
||||
"path": "repo-1",
|
||||
"repoIcon": {
|
||||
"name": "Rocket",
|
||||
"type": "lucide"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
"displayName": "Emoji",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-2",
|
||||
"path": "repo-2",
|
||||
"repoIcon": {
|
||||
"emoji": "🐳",
|
||||
"type": "emoji"
|
||||
}
|
||||
},
|
||||
{
|
||||
"displayName": "Avatar",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-3",
|
||||
"path": "repo-3",
|
||||
"repoIcon": {
|
||||
"label": "acme/orca",
|
||||
"source": "github",
|
||||
"src": "https://github.com/acme.png?size=64",
|
||||
"type": "image"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-4",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"platform": "darwin"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"recording": {
|
||||
@@ -431,34 +423,32 @@
|
||||
{
|
||||
"id": "settings-pending",
|
||||
"observation": {
|
||||
"sender": ["a9add325bb0a", "c752e67787f9", "96e0c54d86b0", "1a160fc2d137"],
|
||||
"payloads": ["19b2979d7fc2", "668a7e313975", "b428bce85ee7", "36a96ddbd396"],
|
||||
"sender": ["a9add325bb0a", "299d571abf13", "902c0b2ed3ce", "7c508088602c"],
|
||||
"payloads": ["19b2979d7fc2", "c1246f5792ee", "8b618db22116", "c1d6fc758acc"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"load": "9270aeb7d9c6"
|
||||
},
|
||||
"state": "6338d57f282f",
|
||||
"effects": ["19bf6d2469a6", "ae27a5b525fa", "77cbfd1f087c", "6bc5e5b7a259"]
|
||||
"state": "910617d262f4",
|
||||
"effects": ["f9217d79f53e", "b4ad28ffe331"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "settled",
|
||||
"observation": {
|
||||
"sender": ["a9add325bb0a", "c752e67787f9", "64f5522cc303", "ee8e16df5187"],
|
||||
"payloads": ["19b2979d7fc2", "668a7e313975", "b428bce85ee7", "36a96ddbd396"],
|
||||
"sender": ["a9add325bb0a", "299d571abf13", "466c0317a24a", "9bc91d02ce24"],
|
||||
"payloads": ["19b2979d7fc2", "c1246f5792ee", "8b618db22116", "c1d6fc758acc"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"load": "eb79a9b3682a"
|
||||
},
|
||||
"state": "c020f709d835",
|
||||
"state": "0fd72e1aba93",
|
||||
"effects": [
|
||||
"19bf6d2469a6",
|
||||
"ae27a5b525fa",
|
||||
"77cbfd1f087c",
|
||||
"6bc5e5b7a259",
|
||||
"6063e534d9db",
|
||||
"bea13a4a2e87",
|
||||
"25b44bca9e45"
|
||||
"f9217d79f53e",
|
||||
"b4ad28ffe331",
|
||||
"e2881f748a46",
|
||||
"d9c464d0dc78",
|
||||
"1c76208c9beb"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,43 +4,14 @@
|
||||
"family": "settings.repo-metadata",
|
||||
"namedDeltas": [],
|
||||
"values": {
|
||||
"03c9a2ba60b9": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 9,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "host.platform"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-4",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"platform": "linux"
|
||||
}
|
||||
}
|
||||
}
|
||||
"00d007a90b2c": {
|
||||
"name": "hostPlatform",
|
||||
"ordinal": 26,
|
||||
"value": "linux"
|
||||
},
|
||||
"0c4296f215a4": {
|
||||
"18762ff064ed": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 8,
|
||||
"ordinal": 6,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
@@ -78,19 +49,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"0ef3d910f42e": {
|
||||
"name": "hostLabelById",
|
||||
"ordinal": 29,
|
||||
"value": [["ssh:ssh-1", "SSH"]]
|
||||
},
|
||||
"19b2979d7fc2": {
|
||||
"name": "repo.list#1",
|
||||
"ordinal": 2,
|
||||
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.list\"}"
|
||||
},
|
||||
"1a160fc2d137": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 9,
|
||||
"1e4a3aa71af5": {
|
||||
"name": "host.platform#2",
|
||||
"ordinal": 20,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
@@ -110,37 +76,79 @@
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-8",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"platform": "linux"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"1ff13ee8621c": {
|
||||
"name": "repoIconsByName",
|
||||
"ordinal": 4,
|
||||
"value": []
|
||||
"299d571abf13": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 5,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "ssh.listTargetSummaries"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-2",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"targets": [
|
||||
{
|
||||
"id": "ssh-1",
|
||||
"label": "SSH"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"2053629911e7": {
|
||||
"name": "repo.list#2",
|
||||
"ordinal": 17,
|
||||
"json": "{\"id\":\"frame-5\",\"deviceToken\":\"recording-device\",\"method\":\"repo.list\"}"
|
||||
},
|
||||
"2f1b95eac8f1": {
|
||||
"name": "repoColorsByName",
|
||||
"ordinal": 3,
|
||||
"306fcf2b8536": {
|
||||
"name": "desktopRepos",
|
||||
"ordinal": 16,
|
||||
"value": [
|
||||
["Local", "#6366f1"],
|
||||
["Remote", "#f97316"]
|
||||
{
|
||||
"displayName": "Local",
|
||||
"executionHostId": "local",
|
||||
"id": "repo-1",
|
||||
"path": "repo-1"
|
||||
},
|
||||
{
|
||||
"displayName": "Remote",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-2",
|
||||
"path": "repo-2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"36a96ddbd396": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 12,
|
||||
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"host.platform\"}"
|
||||
},
|
||||
"376d3874239c": {
|
||||
"name": "repoIconsByName",
|
||||
"ordinal": 19,
|
||||
"value": []
|
||||
"35f30397fcb2": {
|
||||
"name": "repo.list#2",
|
||||
"ordinal": 15,
|
||||
"json": "{\"id\":\"frame-5\",\"deviceToken\":\"recording-device\",\"method\":\"repo.list\"}"
|
||||
},
|
||||
"4526bff18b74": {
|
||||
"name": "repo.list#1",
|
||||
@@ -189,44 +197,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"455855a85ab4": {
|
||||
"name": "settings.get#2",
|
||||
"ordinal": 23,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "settings.get"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"error": {
|
||||
"code": "refused",
|
||||
"message": "settings refused"
|
||||
},
|
||||
"id": "frame-7",
|
||||
"ok": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"4771e3dbbbcf": {
|
||||
"name": "host.platform#2",
|
||||
"ordinal": 24,
|
||||
"5248b37ec085": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 7,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
@@ -250,7 +223,7 @@
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-8",
|
||||
"id": "frame-4",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"platform": "linux"
|
||||
@@ -258,64 +231,106 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"53226cb82ec3": {
|
||||
"name": "hostPlatform",
|
||||
"ordinal": 15,
|
||||
"value": "linux"
|
||||
"661b4721aec6": {
|
||||
"name": "desktopRepos",
|
||||
"ordinal": 3,
|
||||
"value": [
|
||||
{
|
||||
"displayName": "Local",
|
||||
"executionHostId": "local",
|
||||
"id": "repo-1",
|
||||
"path": "repo-1"
|
||||
},
|
||||
{
|
||||
"displayName": "Remote",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-2",
|
||||
"path": "repo-2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"6063e534d9db": {
|
||||
"name": "hostHealthById",
|
||||
"ordinal": 13,
|
||||
"value": []
|
||||
},
|
||||
"6134b73f18d0": {
|
||||
"repoColorsByName": [
|
||||
["Local", "#6366f1"],
|
||||
["Remote", "#f97316"]
|
||||
"68916f125c3c": {
|
||||
"desktopRepos": [
|
||||
{
|
||||
"displayName": "Local",
|
||||
"executionHostId": "local",
|
||||
"id": "repo-1",
|
||||
"path": "repo-1"
|
||||
},
|
||||
{
|
||||
"displayName": "Remote",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-2",
|
||||
"path": "repo-2"
|
||||
}
|
||||
],
|
||||
"repoHostIdByRepoId": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
]
|
||||
},
|
||||
"69f575e15e15": {
|
||||
"name": "settings.get#2",
|
||||
"ordinal": 22,
|
||||
"json": "{\"id\":\"frame-7\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}"
|
||||
},
|
||||
"6d43117882f3": {
|
||||
"desktopRepos": [
|
||||
{
|
||||
"displayName": "Local",
|
||||
"executionHostId": "local",
|
||||
"id": "repo-1",
|
||||
"path": "repo-1"
|
||||
},
|
||||
{
|
||||
"displayName": "Remote",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-2",
|
||||
"path": "repo-2"
|
||||
}
|
||||
],
|
||||
"repoIconsByName": [],
|
||||
"repoIdsByName": [
|
||||
["Local", "repo-1"],
|
||||
["Remote", "repo-2"]
|
||||
]
|
||||
},
|
||||
"668a7e313975": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 10,
|
||||
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.listTargetSummaries\"}"
|
||||
},
|
||||
"7e772c1c111d": {
|
||||
"name": "repoIdsByName",
|
||||
"ordinal": 5,
|
||||
"value": [
|
||||
["Local", "repo-1"],
|
||||
["Remote", "repo-2"]
|
||||
]
|
||||
},
|
||||
"8c5c812580ad": {
|
||||
"name": "host.platform#2",
|
||||
"ordinal": 27,
|
||||
"json": "{\"id\":\"frame-8\",\"deviceToken\":\"recording-device\",\"method\":\"host.platform\"}"
|
||||
},
|
||||
"8ef3b348c179": {
|
||||
"name": "repoHostIdByRepoId",
|
||||
"ordinal": 6,
|
||||
"value": [
|
||||
"hostHealthById": [],
|
||||
"hostLabelById": [["ssh:ssh-1", "SSH"]],
|
||||
"hostPlatform": "linux",
|
||||
"repoHostIdByRepoId": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
]
|
||||
},
|
||||
"9270aeb7d9c6": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
"7c508088602c": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 7,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "host.platform"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
}
|
||||
},
|
||||
"96e0c54d86b0": {
|
||||
"8b618db22116": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}"
|
||||
},
|
||||
"902c0b2ed3ce": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 6,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
@@ -339,78 +354,33 @@
|
||||
"startedAt": 0
|
||||
}
|
||||
},
|
||||
"a67416f78b93": {
|
||||
"name": "hostHealthById",
|
||||
"ordinal": 28,
|
||||
"value": []
|
||||
},
|
||||
"b428bce85ee7": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 11,
|
||||
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}"
|
||||
},
|
||||
"bea13a4a2e87": {
|
||||
"name": "hostLabelById",
|
||||
"ordinal": 14,
|
||||
"value": [["ssh:ssh-1", "SSH"]]
|
||||
},
|
||||
"c497d2d846c9": {
|
||||
"name": "ssh.listTargetSummaries#2",
|
||||
"ordinal": 25,
|
||||
"json": "{\"id\":\"frame-6\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.listTargetSummaries\"}"
|
||||
},
|
||||
"c752e67787f9": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 7,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "ssh.listTargetSummaries"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-2",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"targets": [
|
||||
{
|
||||
"id": "ssh-1",
|
||||
"label": "SSH"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ceef481be25c": {
|
||||
"91e7f4ef9a77": {
|
||||
"name": "hostPlatform",
|
||||
"ordinal": 30,
|
||||
"ordinal": 13,
|
||||
"value": "linux"
|
||||
},
|
||||
"d51254d2537f": {
|
||||
"name": "settings.get#2",
|
||||
"ordinal": 26,
|
||||
"json": "{\"id\":\"frame-7\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}"
|
||||
"9270aeb7d9c6": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
},
|
||||
"d569b5b2500f": {
|
||||
"9b915e5b8831": {
|
||||
"name": "host.platform#2",
|
||||
"ordinal": 23,
|
||||
"json": "{\"id\":\"frame-8\",\"deviceToken\":\"recording-device\",\"method\":\"host.platform\"}"
|
||||
},
|
||||
"a465a07eb4b0": {
|
||||
"name": "hostLabelById",
|
||||
"ordinal": 25,
|
||||
"value": [["ssh:ssh-1", "SSH"]]
|
||||
},
|
||||
"af3366c70550": {
|
||||
"name": "ssh.listTargetSummaries#2",
|
||||
"ordinal": 22,
|
||||
"ordinal": 21,
|
||||
"json": "{\"id\":\"frame-6\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.listTargetSummaries\"}"
|
||||
},
|
||||
"bc879379ce13": {
|
||||
"name": "ssh.listTargetSummaries#2",
|
||||
"ordinal": 18,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
@@ -447,17 +417,63 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"da7cf9752395": {
|
||||
"name": "repoColorsByName",
|
||||
"ordinal": 18,
|
||||
"bd0d18cd2160": {
|
||||
"name": "repoHostIdByRepoId",
|
||||
"ordinal": 17,
|
||||
"value": [
|
||||
["Local", "#6366f1"],
|
||||
["Remote", "#f97316"]
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
]
|
||||
},
|
||||
"df78d2202264": {
|
||||
"c1246f5792ee": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 8,
|
||||
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.listTargetSummaries\"}"
|
||||
},
|
||||
"c1d6fc758acc": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 10,
|
||||
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"host.platform\"}"
|
||||
},
|
||||
"d9c464d0dc78": {
|
||||
"name": "hostLabelById",
|
||||
"ordinal": 12,
|
||||
"value": [["ssh:ssh-1", "SSH"]]
|
||||
},
|
||||
"df3bf9bbb7bf": {
|
||||
"name": "repo.list#2",
|
||||
"ordinal": 16,
|
||||
"ordinal": 14,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "repo.list"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
}
|
||||
},
|
||||
"e2881f748a46": {
|
||||
"name": "hostHealthById",
|
||||
"ordinal": 11,
|
||||
"value": []
|
||||
},
|
||||
"e3e3d558d06c": {
|
||||
"name": "repo.list#2",
|
||||
"ordinal": 14,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
@@ -502,55 +518,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ea2ced0895f3": {
|
||||
"e49da3e1abca": {
|
||||
"name": "repoHostIdByRepoId",
|
||||
"ordinal": 21,
|
||||
"ordinal": 4,
|
||||
"value": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
]
|
||||
},
|
||||
"eb79a9b3682a": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"$rpc": "undefined"
|
||||
}
|
||||
"e7d590a1a17e": {
|
||||
"name": "hostHealthById",
|
||||
"ordinal": 24,
|
||||
"value": []
|
||||
},
|
||||
"eb885d2ceee5": {
|
||||
"name": "repoIdsByName",
|
||||
"ordinal": 20,
|
||||
"value": [
|
||||
["Local", "repo-1"],
|
||||
["Remote", "repo-2"]
|
||||
]
|
||||
},
|
||||
"f4667dda0187": {
|
||||
"hostHealthById": [],
|
||||
"hostLabelById": [["ssh:ssh-1", "SSH"]],
|
||||
"hostPlatform": "linux",
|
||||
"repoColorsByName": [
|
||||
["Local", "#6366f1"],
|
||||
["Remote", "#f97316"]
|
||||
],
|
||||
"repoHostIdByRepoId": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
],
|
||||
"repoIconsByName": [],
|
||||
"repoIdsByName": [
|
||||
["Local", "repo-1"],
|
||||
["Remote", "repo-2"]
|
||||
]
|
||||
},
|
||||
"fa585aceeec2": {
|
||||
"name": "repo.list#2",
|
||||
"ordinal": 16,
|
||||
"eab4940c8ca3": {
|
||||
"name": "settings.get#2",
|
||||
"ordinal": 19,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "repo.list"
|
||||
"value": "settings.get"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
@@ -566,8 +553,25 @@
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"error": {
|
||||
"code": "refused",
|
||||
"message": "settings refused"
|
||||
},
|
||||
"id": "frame-7",
|
||||
"ok": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"eb79a9b3682a": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"$rpc": "undefined"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -577,55 +581,51 @@
|
||||
{
|
||||
"id": "settings-pending",
|
||||
"observation": {
|
||||
"sender": ["4526bff18b74", "c752e67787f9", "96e0c54d86b0", "1a160fc2d137"],
|
||||
"payloads": ["19b2979d7fc2", "668a7e313975", "b428bce85ee7", "36a96ddbd396"],
|
||||
"sender": ["4526bff18b74", "299d571abf13", "902c0b2ed3ce", "7c508088602c"],
|
||||
"payloads": ["19b2979d7fc2", "c1246f5792ee", "8b618db22116", "c1d6fc758acc"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"load": "9270aeb7d9c6"
|
||||
},
|
||||
"state": "6134b73f18d0",
|
||||
"effects": ["2f1b95eac8f1", "1ff13ee8621c", "7e772c1c111d", "8ef3b348c179"]
|
||||
"state": "68916f125c3c",
|
||||
"effects": ["661b4721aec6", "e49da3e1abca"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "settled",
|
||||
"observation": {
|
||||
"sender": ["4526bff18b74", "c752e67787f9", "0c4296f215a4", "03c9a2ba60b9"],
|
||||
"payloads": ["19b2979d7fc2", "668a7e313975", "b428bce85ee7", "36a96ddbd396"],
|
||||
"sender": ["4526bff18b74", "299d571abf13", "18762ff064ed", "5248b37ec085"],
|
||||
"payloads": ["19b2979d7fc2", "c1246f5792ee", "8b618db22116", "c1d6fc758acc"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"load": "eb79a9b3682a"
|
||||
},
|
||||
"state": "f4667dda0187",
|
||||
"state": "6d43117882f3",
|
||||
"effects": [
|
||||
"2f1b95eac8f1",
|
||||
"1ff13ee8621c",
|
||||
"7e772c1c111d",
|
||||
"8ef3b348c179",
|
||||
"6063e534d9db",
|
||||
"bea13a4a2e87",
|
||||
"53226cb82ec3"
|
||||
"661b4721aec6",
|
||||
"e49da3e1abca",
|
||||
"e2881f748a46",
|
||||
"d9c464d0dc78",
|
||||
"91e7f4ef9a77"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "data-present",
|
||||
"observation": {
|
||||
"sender": ["4526bff18b74", "c752e67787f9", "0c4296f215a4", "03c9a2ba60b9"],
|
||||
"payloads": ["19b2979d7fc2", "668a7e313975", "b428bce85ee7", "36a96ddbd396"],
|
||||
"sender": ["4526bff18b74", "299d571abf13", "18762ff064ed", "5248b37ec085"],
|
||||
"payloads": ["19b2979d7fc2", "c1246f5792ee", "8b618db22116", "c1d6fc758acc"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"load": "eb79a9b3682a"
|
||||
},
|
||||
"state": "f4667dda0187",
|
||||
"state": "6d43117882f3",
|
||||
"effects": [
|
||||
"2f1b95eac8f1",
|
||||
"1ff13ee8621c",
|
||||
"7e772c1c111d",
|
||||
"8ef3b348c179",
|
||||
"6063e534d9db",
|
||||
"bea13a4a2e87",
|
||||
"53226cb82ec3"
|
||||
"661b4721aec6",
|
||||
"e49da3e1abca",
|
||||
"e2881f748a46",
|
||||
"d9c464d0dc78",
|
||||
"91e7f4ef9a77"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -634,32 +634,30 @@
|
||||
"observation": {
|
||||
"sender": [
|
||||
"4526bff18b74",
|
||||
"c752e67787f9",
|
||||
"0c4296f215a4",
|
||||
"03c9a2ba60b9",
|
||||
"fa585aceeec2"
|
||||
"299d571abf13",
|
||||
"18762ff064ed",
|
||||
"5248b37ec085",
|
||||
"df3bf9bbb7bf"
|
||||
],
|
||||
"payloads": [
|
||||
"19b2979d7fc2",
|
||||
"668a7e313975",
|
||||
"b428bce85ee7",
|
||||
"36a96ddbd396",
|
||||
"2053629911e7"
|
||||
"c1246f5792ee",
|
||||
"8b618db22116",
|
||||
"c1d6fc758acc",
|
||||
"35f30397fcb2"
|
||||
],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"load": "eb79a9b3682a",
|
||||
"reload": "9270aeb7d9c6"
|
||||
},
|
||||
"state": "f4667dda0187",
|
||||
"state": "6d43117882f3",
|
||||
"effects": [
|
||||
"2f1b95eac8f1",
|
||||
"1ff13ee8621c",
|
||||
"7e772c1c111d",
|
||||
"8ef3b348c179",
|
||||
"6063e534d9db",
|
||||
"bea13a4a2e87",
|
||||
"53226cb82ec3"
|
||||
"661b4721aec6",
|
||||
"e49da3e1abca",
|
||||
"e2881f748a46",
|
||||
"d9c464d0dc78",
|
||||
"91e7f4ef9a77"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -668,45 +666,41 @@
|
||||
"observation": {
|
||||
"sender": [
|
||||
"4526bff18b74",
|
||||
"c752e67787f9",
|
||||
"0c4296f215a4",
|
||||
"03c9a2ba60b9",
|
||||
"df78d2202264",
|
||||
"d569b5b2500f",
|
||||
"455855a85ab4",
|
||||
"4771e3dbbbcf"
|
||||
"299d571abf13",
|
||||
"18762ff064ed",
|
||||
"5248b37ec085",
|
||||
"e3e3d558d06c",
|
||||
"bc879379ce13",
|
||||
"eab4940c8ca3",
|
||||
"1e4a3aa71af5"
|
||||
],
|
||||
"payloads": [
|
||||
"19b2979d7fc2",
|
||||
"668a7e313975",
|
||||
"b428bce85ee7",
|
||||
"36a96ddbd396",
|
||||
"2053629911e7",
|
||||
"c497d2d846c9",
|
||||
"d51254d2537f",
|
||||
"8c5c812580ad"
|
||||
"c1246f5792ee",
|
||||
"8b618db22116",
|
||||
"c1d6fc758acc",
|
||||
"35f30397fcb2",
|
||||
"af3366c70550",
|
||||
"69f575e15e15",
|
||||
"9b915e5b8831"
|
||||
],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"load": "eb79a9b3682a",
|
||||
"reload": "eb79a9b3682a"
|
||||
},
|
||||
"state": "f4667dda0187",
|
||||
"state": "6d43117882f3",
|
||||
"effects": [
|
||||
"2f1b95eac8f1",
|
||||
"1ff13ee8621c",
|
||||
"7e772c1c111d",
|
||||
"8ef3b348c179",
|
||||
"6063e534d9db",
|
||||
"bea13a4a2e87",
|
||||
"53226cb82ec3",
|
||||
"da7cf9752395",
|
||||
"376d3874239c",
|
||||
"eb885d2ceee5",
|
||||
"ea2ced0895f3",
|
||||
"a67416f78b93",
|
||||
"0ef3d910f42e",
|
||||
"ceef481be25c"
|
||||
"661b4721aec6",
|
||||
"e49da3e1abca",
|
||||
"e2881f748a46",
|
||||
"d9c464d0dc78",
|
||||
"91e7f4ef9a77",
|
||||
"306fcf2b8536",
|
||||
"bd0d18cd2160",
|
||||
"e7d590a1a17e",
|
||||
"a465a07eb4b0",
|
||||
"00d007a90b2c"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,83 +4,18 @@
|
||||
"family": "settings.repo-metadata",
|
||||
"namedDeltas": [],
|
||||
"values": {
|
||||
"03c9a2ba60b9": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 9,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "host.platform"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-4",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"platform": "linux"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"19b2979d7fc2": {
|
||||
"name": "repo.list#1",
|
||||
"ordinal": 2,
|
||||
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.list\"}"
|
||||
},
|
||||
"1a160fc2d137": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 9,
|
||||
"299d571abf13": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 5,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "host.platform"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
}
|
||||
},
|
||||
"1ff13ee8621c": {
|
||||
"name": "repoIconsByName",
|
||||
"ordinal": 4,
|
||||
"value": []
|
||||
},
|
||||
"2e2ca6f7dc3f": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 8,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "settings.get"
|
||||
"value": "ssh.listTargetSummaries"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
@@ -100,28 +35,19 @@
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"error": {
|
||||
"code": "refused",
|
||||
"message": "settings refused"
|
||||
},
|
||||
"id": "frame-3",
|
||||
"ok": false
|
||||
"id": "frame-2",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"targets": [
|
||||
{
|
||||
"id": "ssh-1",
|
||||
"label": "SSH"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"2f1b95eac8f1": {
|
||||
"name": "repoColorsByName",
|
||||
"ordinal": 3,
|
||||
"value": [
|
||||
["Local", "#6366f1"],
|
||||
["Remote", "#f97316"]
|
||||
]
|
||||
},
|
||||
"36a96ddbd396": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 12,
|
||||
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"host.platform\"}"
|
||||
},
|
||||
"4526bff18b74": {
|
||||
"name": "repo.list#1",
|
||||
"ordinal": 1,
|
||||
@@ -169,59 +95,170 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"53226cb82ec3": {
|
||||
"name": "hostPlatform",
|
||||
"ordinal": 15,
|
||||
"value": "linux"
|
||||
"5248b37ec085": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 7,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "host.platform"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-4",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"platform": "linux"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"6063e534d9db": {
|
||||
"name": "hostHealthById",
|
||||
"ordinal": 13,
|
||||
"value": []
|
||||
"61b27dbdf3a7": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 6,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "settings.get"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"error": {
|
||||
"code": "refused",
|
||||
"message": "settings refused"
|
||||
},
|
||||
"id": "frame-3",
|
||||
"ok": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"6134b73f18d0": {
|
||||
"repoColorsByName": [
|
||||
["Local", "#6366f1"],
|
||||
["Remote", "#f97316"]
|
||||
"661b4721aec6": {
|
||||
"name": "desktopRepos",
|
||||
"ordinal": 3,
|
||||
"value": [
|
||||
{
|
||||
"displayName": "Local",
|
||||
"executionHostId": "local",
|
||||
"id": "repo-1",
|
||||
"path": "repo-1"
|
||||
},
|
||||
{
|
||||
"displayName": "Remote",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-2",
|
||||
"path": "repo-2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"68916f125c3c": {
|
||||
"desktopRepos": [
|
||||
{
|
||||
"displayName": "Local",
|
||||
"executionHostId": "local",
|
||||
"id": "repo-1",
|
||||
"path": "repo-1"
|
||||
},
|
||||
{
|
||||
"displayName": "Remote",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-2",
|
||||
"path": "repo-2"
|
||||
}
|
||||
],
|
||||
"repoHostIdByRepoId": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
]
|
||||
},
|
||||
"6d43117882f3": {
|
||||
"desktopRepos": [
|
||||
{
|
||||
"displayName": "Local",
|
||||
"executionHostId": "local",
|
||||
"id": "repo-1",
|
||||
"path": "repo-1"
|
||||
},
|
||||
{
|
||||
"displayName": "Remote",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-2",
|
||||
"path": "repo-2"
|
||||
}
|
||||
],
|
||||
"repoIconsByName": [],
|
||||
"repoIdsByName": [
|
||||
["Local", "repo-1"],
|
||||
["Remote", "repo-2"]
|
||||
]
|
||||
},
|
||||
"668a7e313975": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 10,
|
||||
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.listTargetSummaries\"}"
|
||||
},
|
||||
"7e772c1c111d": {
|
||||
"name": "repoIdsByName",
|
||||
"ordinal": 5,
|
||||
"value": [
|
||||
["Local", "repo-1"],
|
||||
["Remote", "repo-2"]
|
||||
]
|
||||
},
|
||||
"8ef3b348c179": {
|
||||
"name": "repoHostIdByRepoId",
|
||||
"ordinal": 6,
|
||||
"value": [
|
||||
"hostHealthById": [],
|
||||
"hostLabelById": [["ssh:ssh-1", "SSH"]],
|
||||
"hostPlatform": "linux",
|
||||
"repoHostIdByRepoId": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
]
|
||||
},
|
||||
"9270aeb7d9c6": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
"7c508088602c": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 7,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "host.platform"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
}
|
||||
},
|
||||
"96e0c54d86b0": {
|
||||
"8b618db22116": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}"
|
||||
},
|
||||
"902c0b2ed3ce": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 6,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
@@ -245,54 +282,42 @@
|
||||
"startedAt": 0
|
||||
}
|
||||
},
|
||||
"b428bce85ee7": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 11,
|
||||
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}"
|
||||
"91e7f4ef9a77": {
|
||||
"name": "hostPlatform",
|
||||
"ordinal": 13,
|
||||
"value": "linux"
|
||||
},
|
||||
"bea13a4a2e87": {
|
||||
"9270aeb7d9c6": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
},
|
||||
"c1246f5792ee": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 8,
|
||||
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.listTargetSummaries\"}"
|
||||
},
|
||||
"c1d6fc758acc": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 10,
|
||||
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"host.platform\"}"
|
||||
},
|
||||
"d9c464d0dc78": {
|
||||
"name": "hostLabelById",
|
||||
"ordinal": 14,
|
||||
"ordinal": 12,
|
||||
"value": [["ssh:ssh-1", "SSH"]]
|
||||
},
|
||||
"c752e67787f9": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 7,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "ssh.listTargetSummaries"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-2",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"targets": [
|
||||
{
|
||||
"id": "ssh-1",
|
||||
"label": "SSH"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
"e2881f748a46": {
|
||||
"name": "hostHealthById",
|
||||
"ordinal": 11,
|
||||
"value": []
|
||||
},
|
||||
"e49da3e1abca": {
|
||||
"name": "repoHostIdByRepoId",
|
||||
"ordinal": 4,
|
||||
"value": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
]
|
||||
},
|
||||
"eb79a9b3682a": {
|
||||
"status": "fulfilled",
|
||||
@@ -301,24 +326,6 @@
|
||||
"value": {
|
||||
"$rpc": "undefined"
|
||||
}
|
||||
},
|
||||
"f4667dda0187": {
|
||||
"hostHealthById": [],
|
||||
"hostLabelById": [["ssh:ssh-1", "SSH"]],
|
||||
"hostPlatform": "linux",
|
||||
"repoColorsByName": [
|
||||
["Local", "#6366f1"],
|
||||
["Remote", "#f97316"]
|
||||
],
|
||||
"repoHostIdByRepoId": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
],
|
||||
"repoIconsByName": [],
|
||||
"repoIdsByName": [
|
||||
["Local", "repo-1"],
|
||||
["Remote", "repo-2"]
|
||||
]
|
||||
}
|
||||
},
|
||||
"recording": {
|
||||
@@ -327,34 +334,32 @@
|
||||
{
|
||||
"id": "settings-pending",
|
||||
"observation": {
|
||||
"sender": ["4526bff18b74", "c752e67787f9", "96e0c54d86b0", "1a160fc2d137"],
|
||||
"payloads": ["19b2979d7fc2", "668a7e313975", "b428bce85ee7", "36a96ddbd396"],
|
||||
"sender": ["4526bff18b74", "299d571abf13", "902c0b2ed3ce", "7c508088602c"],
|
||||
"payloads": ["19b2979d7fc2", "c1246f5792ee", "8b618db22116", "c1d6fc758acc"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"load": "9270aeb7d9c6"
|
||||
},
|
||||
"state": "6134b73f18d0",
|
||||
"effects": ["2f1b95eac8f1", "1ff13ee8621c", "7e772c1c111d", "8ef3b348c179"]
|
||||
"state": "68916f125c3c",
|
||||
"effects": ["661b4721aec6", "e49da3e1abca"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "settled",
|
||||
"observation": {
|
||||
"sender": ["4526bff18b74", "c752e67787f9", "2e2ca6f7dc3f", "03c9a2ba60b9"],
|
||||
"payloads": ["19b2979d7fc2", "668a7e313975", "b428bce85ee7", "36a96ddbd396"],
|
||||
"sender": ["4526bff18b74", "299d571abf13", "61b27dbdf3a7", "5248b37ec085"],
|
||||
"payloads": ["19b2979d7fc2", "c1246f5792ee", "8b618db22116", "c1d6fc758acc"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"load": "eb79a9b3682a"
|
||||
},
|
||||
"state": "f4667dda0187",
|
||||
"state": "6d43117882f3",
|
||||
"effects": [
|
||||
"2f1b95eac8f1",
|
||||
"1ff13ee8621c",
|
||||
"7e772c1c111d",
|
||||
"8ef3b348c179",
|
||||
"6063e534d9db",
|
||||
"bea13a4a2e87",
|
||||
"53226cb82ec3"
|
||||
"661b4721aec6",
|
||||
"e49da3e1abca",
|
||||
"e2881f748a46",
|
||||
"d9c464d0dc78",
|
||||
"91e7f4ef9a77"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
"ordinal": 2,
|
||||
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.list\"}"
|
||||
},
|
||||
"1a160fc2d137": {
|
||||
"1e027d0127f3": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 9,
|
||||
"ordinal": 7,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
@@ -30,35 +30,118 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "rejected",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"error": {
|
||||
"category": "Error",
|
||||
"message": "Connection closed",
|
||||
"isRpcDeliveryUnknown": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"1f909a505b54": {
|
||||
"name": "hostPlatform",
|
||||
"ordinal": 13,
|
||||
"value": {
|
||||
"$rpc": "null"
|
||||
}
|
||||
},
|
||||
"2207323d0bd5": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 5,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "ssh.listTargetSummaries"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "rejected",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"error": {
|
||||
"category": "Error",
|
||||
"message": "Connection closed",
|
||||
"isRpcDeliveryUnknown": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"302f82cb9a68": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 6,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "settings.get"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "rejected",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"error": {
|
||||
"category": "Error",
|
||||
"message": "Connection closed",
|
||||
"isRpcDeliveryUnknown": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"35960f0ff1b9": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 5,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "ssh.listTargetSummaries"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
}
|
||||
},
|
||||
"1ff13ee8621c": {
|
||||
"name": "repoIconsByName",
|
||||
"ordinal": 4,
|
||||
"value": []
|
||||
},
|
||||
"2bd489a9fa29": {
|
||||
"repoColorsByName": [
|
||||
["Remote", "#f97316"],
|
||||
["Remote folder", "#ec4899"]
|
||||
],
|
||||
"repoHostIdByRepoId": [
|
||||
["repo-1", "ssh:ssh-1"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
],
|
||||
"repoIconsByName": [],
|
||||
"repoIdsByName": [
|
||||
["Remote", "repo-1"],
|
||||
["Remote folder", "repo-2"]
|
||||
]
|
||||
},
|
||||
"36a96ddbd396": {
|
||||
"name": "host.platform#1",
|
||||
"379c0eb6dc14": {
|
||||
"name": "hostLabelById",
|
||||
"ordinal": 12,
|
||||
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"host.platform\"}"
|
||||
"value": []
|
||||
},
|
||||
"4b6956e45bac": {
|
||||
"name": "repo.list#1",
|
||||
@@ -105,41 +188,27 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"4bf1ed33de14": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 7,
|
||||
"args": [
|
||||
"6e0383a0869e": {
|
||||
"name": "desktopRepos",
|
||||
"ordinal": 3,
|
||||
"value": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "ssh.listTargetSummaries"
|
||||
"displayName": "Remote",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-1",
|
||||
"path": "repo-1"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
"displayName": "Remote folder",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-2",
|
||||
"path": "repo-2"
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "rejected",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"error": {
|
||||
"category": "Error",
|
||||
"message": "Connection closed",
|
||||
"isRpcDeliveryUnknown": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"53dea128b181": {
|
||||
"7c508088602c": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 9,
|
||||
"ordinal": 7,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
@@ -159,33 +228,22 @@
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "rejected",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"error": {
|
||||
"category": "Error",
|
||||
"message": "Connection closed",
|
||||
"isRpcDeliveryUnknown": true
|
||||
}
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
}
|
||||
},
|
||||
"6063e534d9db": {
|
||||
"name": "hostHealthById",
|
||||
"ordinal": 13,
|
||||
"value": []
|
||||
"8b618db22116": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 9,
|
||||
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}"
|
||||
},
|
||||
"668a7e313975": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 10,
|
||||
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.listTargetSummaries\"}"
|
||||
},
|
||||
"6a227ad27189": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 7,
|
||||
"902c0b2ed3ce": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 6,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "ssh.listTargetSummaries"
|
||||
"value": "settings.get"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
@@ -209,96 +267,48 @@
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
},
|
||||
"937107a86348": {
|
||||
"name": "repoIdsByName",
|
||||
"ordinal": 5,
|
||||
"value": [
|
||||
["Remote", "repo-1"],
|
||||
["Remote folder", "repo-2"]
|
||||
]
|
||||
},
|
||||
"96e0c54d86b0": {
|
||||
"name": "settings.get#1",
|
||||
"c1246f5792ee": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 8,
|
||||
"args": [
|
||||
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.listTargetSummaries\"}"
|
||||
},
|
||||
"c1d6fc758acc": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 10,
|
||||
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"host.platform\"}"
|
||||
},
|
||||
"d23c5c4cde9a": {
|
||||
"desktopRepos": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "settings.get"
|
||||
"displayName": "Remote",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-1",
|
||||
"path": "repo-1"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
"displayName": "Remote folder",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-2",
|
||||
"path": "repo-2"
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
}
|
||||
},
|
||||
"9d20695fdb34": {
|
||||
"name": "hostPlatform",
|
||||
"ordinal": 15,
|
||||
"value": {
|
||||
"$rpc": "null"
|
||||
}
|
||||
},
|
||||
"a086cfd03185": {
|
||||
"name": "repoHostIdByRepoId",
|
||||
"ordinal": 6,
|
||||
"value": [
|
||||
"repoHostIdByRepoId": [
|
||||
["repo-1", "ssh:ssh-1"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
]
|
||||
},
|
||||
"b428bce85ee7": {
|
||||
"name": "settings.get#1",
|
||||
"e2881f748a46": {
|
||||
"name": "hostHealthById",
|
||||
"ordinal": 11,
|
||||
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}"
|
||||
},
|
||||
"daecdc0e521b": {
|
||||
"name": "hostLabelById",
|
||||
"ordinal": 14,
|
||||
"value": []
|
||||
},
|
||||
"dd3998116e8e": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 8,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "settings.get"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "rejected",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"error": {
|
||||
"category": "Error",
|
||||
"message": "Connection closed",
|
||||
"isRpcDeliveryUnknown": true
|
||||
}
|
||||
}
|
||||
"e7dd8e429107": {
|
||||
"name": "repoHostIdByRepoId",
|
||||
"ordinal": 4,
|
||||
"value": [
|
||||
["repo-1", "ssh:ssh-1"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
]
|
||||
},
|
||||
"eb79a9b3682a": {
|
||||
"status": "fulfilled",
|
||||
@@ -307,14 +317,6 @@
|
||||
"value": {
|
||||
"$rpc": "undefined"
|
||||
}
|
||||
},
|
||||
"fc0785cc0b08": {
|
||||
"name": "repoColorsByName",
|
||||
"ordinal": 3,
|
||||
"value": [
|
||||
["Remote", "#f97316"],
|
||||
["Remote folder", "#ec4899"]
|
||||
]
|
||||
}
|
||||
},
|
||||
"recording": {
|
||||
@@ -323,34 +325,32 @@
|
||||
{
|
||||
"id": "single-host-without-label-lookups",
|
||||
"observation": {
|
||||
"sender": ["4b6956e45bac", "6a227ad27189", "96e0c54d86b0", "1a160fc2d137"],
|
||||
"payloads": ["19b2979d7fc2", "668a7e313975", "b428bce85ee7", "36a96ddbd396"],
|
||||
"sender": ["4b6956e45bac", "35960f0ff1b9", "902c0b2ed3ce", "7c508088602c"],
|
||||
"payloads": ["19b2979d7fc2", "c1246f5792ee", "8b618db22116", "c1d6fc758acc"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"load": "9270aeb7d9c6"
|
||||
},
|
||||
"state": "2bd489a9fa29",
|
||||
"effects": ["fc0785cc0b08", "1ff13ee8621c", "937107a86348", "a086cfd03185"]
|
||||
"state": "d23c5c4cde9a",
|
||||
"effects": ["6e0383a0869e", "e7dd8e429107"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "cleanup",
|
||||
"observation": {
|
||||
"sender": ["4b6956e45bac", "4bf1ed33de14", "dd3998116e8e", "53dea128b181"],
|
||||
"payloads": ["19b2979d7fc2", "668a7e313975", "b428bce85ee7", "36a96ddbd396"],
|
||||
"sender": ["4b6956e45bac", "2207323d0bd5", "302f82cb9a68", "1e027d0127f3"],
|
||||
"payloads": ["19b2979d7fc2", "c1246f5792ee", "8b618db22116", "c1d6fc758acc"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"load": "eb79a9b3682a"
|
||||
},
|
||||
"state": "2bd489a9fa29",
|
||||
"state": "d23c5c4cde9a",
|
||||
"effects": [
|
||||
"fc0785cc0b08",
|
||||
"1ff13ee8621c",
|
||||
"937107a86348",
|
||||
"a086cfd03185",
|
||||
"6063e534d9db",
|
||||
"daecdc0e521b",
|
||||
"9d20695fdb34"
|
||||
"6e0383a0869e",
|
||||
"e7dd8e429107",
|
||||
"e2881f748a46",
|
||||
"379c0eb6dc14",
|
||||
"1f909a505b54"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,18 @@
|
||||
"family": "settings.repo-metadata",
|
||||
"namedDeltas": [],
|
||||
"values": {
|
||||
"03c9a2ba60b9": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 9,
|
||||
"19b2979d7fc2": {
|
||||
"name": "repo.list#1",
|
||||
"ordinal": 2,
|
||||
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.list\"}"
|
||||
},
|
||||
"299d571abf13": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 5,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "host.platform"
|
||||
"value": "ssh.listTargetSummaries"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
@@ -30,63 +35,19 @@
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-4",
|
||||
"id": "frame-2",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"platform": "linux"
|
||||
"targets": [
|
||||
{
|
||||
"id": "ssh-1",
|
||||
"label": "SSH"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"19b2979d7fc2": {
|
||||
"name": "repo.list#1",
|
||||
"ordinal": 2,
|
||||
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.list\"}"
|
||||
},
|
||||
"1a160fc2d137": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 9,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "host.platform"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
}
|
||||
},
|
||||
"1ff13ee8621c": {
|
||||
"name": "repoIconsByName",
|
||||
"ordinal": 4,
|
||||
"value": []
|
||||
},
|
||||
"2f1b95eac8f1": {
|
||||
"name": "repoColorsByName",
|
||||
"ordinal": 3,
|
||||
"value": [
|
||||
["Local", "#6366f1"],
|
||||
["Remote", "#f97316"]
|
||||
]
|
||||
},
|
||||
"36a96ddbd396": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 12,
|
||||
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"host.platform\"}"
|
||||
},
|
||||
"4526bff18b74": {
|
||||
"name": "repo.list#1",
|
||||
"ordinal": 1,
|
||||
@@ -134,14 +95,180 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"53226cb82ec3": {
|
||||
"5248b37ec085": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 7,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "host.platform"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-4",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"platform": "linux"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"661b4721aec6": {
|
||||
"name": "desktopRepos",
|
||||
"ordinal": 3,
|
||||
"value": [
|
||||
{
|
||||
"displayName": "Local",
|
||||
"executionHostId": "local",
|
||||
"id": "repo-1",
|
||||
"path": "repo-1"
|
||||
},
|
||||
{
|
||||
"displayName": "Remote",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-2",
|
||||
"path": "repo-2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"68916f125c3c": {
|
||||
"desktopRepos": [
|
||||
{
|
||||
"displayName": "Local",
|
||||
"executionHostId": "local",
|
||||
"id": "repo-1",
|
||||
"path": "repo-1"
|
||||
},
|
||||
{
|
||||
"displayName": "Remote",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-2",
|
||||
"path": "repo-2"
|
||||
}
|
||||
],
|
||||
"repoHostIdByRepoId": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
]
|
||||
},
|
||||
"6d43117882f3": {
|
||||
"desktopRepos": [
|
||||
{
|
||||
"displayName": "Local",
|
||||
"executionHostId": "local",
|
||||
"id": "repo-1",
|
||||
"path": "repo-1"
|
||||
},
|
||||
{
|
||||
"displayName": "Remote",
|
||||
"executionHostId": "ssh:ssh-1",
|
||||
"id": "repo-2",
|
||||
"path": "repo-2"
|
||||
}
|
||||
],
|
||||
"hostHealthById": [],
|
||||
"hostLabelById": [["ssh:ssh-1", "SSH"]],
|
||||
"hostPlatform": "linux",
|
||||
"repoHostIdByRepoId": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
]
|
||||
},
|
||||
"7c508088602c": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 7,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "host.platform"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
}
|
||||
},
|
||||
"8b618db22116": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 9,
|
||||
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}"
|
||||
},
|
||||
"902c0b2ed3ce": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 6,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "settings.get"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
}
|
||||
},
|
||||
"91e7f4ef9a77": {
|
||||
"name": "hostPlatform",
|
||||
"ordinal": 15,
|
||||
"ordinal": 13,
|
||||
"value": "linux"
|
||||
},
|
||||
"6021c58b2fd2": {
|
||||
"name": "settings.get#1",
|
||||
"9270aeb7d9c6": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
},
|
||||
"c1246f5792ee": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 8,
|
||||
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.listTargetSummaries\"}"
|
||||
},
|
||||
"c1d6fc758acc": {
|
||||
"name": "host.platform#1",
|
||||
"ordinal": 10,
|
||||
"json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"host.platform\"}"
|
||||
},
|
||||
"ce32fe5b8377": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 6,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
@@ -171,125 +298,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"6063e534d9db": {
|
||||
"name": "hostHealthById",
|
||||
"ordinal": 13,
|
||||
"value": []
|
||||
},
|
||||
"6134b73f18d0": {
|
||||
"repoColorsByName": [
|
||||
["Local", "#6366f1"],
|
||||
["Remote", "#f97316"]
|
||||
],
|
||||
"repoHostIdByRepoId": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
],
|
||||
"repoIconsByName": [],
|
||||
"repoIdsByName": [
|
||||
["Local", "repo-1"],
|
||||
["Remote", "repo-2"]
|
||||
]
|
||||
},
|
||||
"668a7e313975": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 10,
|
||||
"json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.listTargetSummaries\"}"
|
||||
},
|
||||
"7e772c1c111d": {
|
||||
"name": "repoIdsByName",
|
||||
"ordinal": 5,
|
||||
"value": [
|
||||
["Local", "repo-1"],
|
||||
["Remote", "repo-2"]
|
||||
]
|
||||
},
|
||||
"8ef3b348c179": {
|
||||
"name": "repoHostIdByRepoId",
|
||||
"ordinal": 6,
|
||||
"value": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
]
|
||||
},
|
||||
"9270aeb7d9c6": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
},
|
||||
"96e0c54d86b0": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 8,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "settings.get"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "pending",
|
||||
"startedAt": 0
|
||||
}
|
||||
},
|
||||
"b428bce85ee7": {
|
||||
"name": "settings.get#1",
|
||||
"ordinal": 11,
|
||||
"json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}"
|
||||
},
|
||||
"bea13a4a2e87": {
|
||||
"d9c464d0dc78": {
|
||||
"name": "hostLabelById",
|
||||
"ordinal": 14,
|
||||
"ordinal": 12,
|
||||
"value": [["ssh:ssh-1", "SSH"]]
|
||||
},
|
||||
"c752e67787f9": {
|
||||
"name": "ssh.listTargetSummaries#1",
|
||||
"ordinal": 7,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "ssh.listTargetSummaries"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-2",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"targets": [
|
||||
{
|
||||
"id": "ssh-1",
|
||||
"label": "SSH"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
"e2881f748a46": {
|
||||
"name": "hostHealthById",
|
||||
"ordinal": 11,
|
||||
"value": []
|
||||
},
|
||||
"e49da3e1abca": {
|
||||
"name": "repoHostIdByRepoId",
|
||||
"ordinal": 4,
|
||||
"value": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
]
|
||||
},
|
||||
"eb79a9b3682a": {
|
||||
"status": "fulfilled",
|
||||
@@ -298,24 +323,6 @@
|
||||
"value": {
|
||||
"$rpc": "undefined"
|
||||
}
|
||||
},
|
||||
"f4667dda0187": {
|
||||
"hostHealthById": [],
|
||||
"hostLabelById": [["ssh:ssh-1", "SSH"]],
|
||||
"hostPlatform": "linux",
|
||||
"repoColorsByName": [
|
||||
["Local", "#6366f1"],
|
||||
["Remote", "#f97316"]
|
||||
],
|
||||
"repoHostIdByRepoId": [
|
||||
["repo-1", "local"],
|
||||
["repo-2", "ssh:ssh-1"]
|
||||
],
|
||||
"repoIconsByName": [],
|
||||
"repoIdsByName": [
|
||||
["Local", "repo-1"],
|
||||
["Remote", "repo-2"]
|
||||
]
|
||||
}
|
||||
},
|
||||
"recording": {
|
||||
@@ -324,34 +331,32 @@
|
||||
{
|
||||
"id": "settings-pending",
|
||||
"observation": {
|
||||
"sender": ["4526bff18b74", "c752e67787f9", "96e0c54d86b0", "1a160fc2d137"],
|
||||
"payloads": ["19b2979d7fc2", "668a7e313975", "b428bce85ee7", "36a96ddbd396"],
|
||||
"sender": ["4526bff18b74", "299d571abf13", "902c0b2ed3ce", "7c508088602c"],
|
||||
"payloads": ["19b2979d7fc2", "c1246f5792ee", "8b618db22116", "c1d6fc758acc"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"load": "9270aeb7d9c6"
|
||||
},
|
||||
"state": "6134b73f18d0",
|
||||
"effects": ["2f1b95eac8f1", "1ff13ee8621c", "7e772c1c111d", "8ef3b348c179"]
|
||||
"state": "68916f125c3c",
|
||||
"effects": ["661b4721aec6", "e49da3e1abca"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "settled",
|
||||
"observation": {
|
||||
"sender": ["4526bff18b74", "c752e67787f9", "6021c58b2fd2", "03c9a2ba60b9"],
|
||||
"payloads": ["19b2979d7fc2", "668a7e313975", "b428bce85ee7", "36a96ddbd396"],
|
||||
"sender": ["4526bff18b74", "299d571abf13", "ce32fe5b8377", "5248b37ec085"],
|
||||
"payloads": ["19b2979d7fc2", "c1246f5792ee", "8b618db22116", "c1d6fc758acc"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a",
|
||||
"load": "eb79a9b3682a"
|
||||
},
|
||||
"state": "f4667dda0187",
|
||||
"state": "6d43117882f3",
|
||||
"effects": [
|
||||
"2f1b95eac8f1",
|
||||
"1ff13ee8621c",
|
||||
"7e772c1c111d",
|
||||
"8ef3b348c179",
|
||||
"6063e534d9db",
|
||||
"bea13a4a2e87",
|
||||
"53226cb82ec3"
|
||||
"661b4721aec6",
|
||||
"e49da3e1abca",
|
||||
"e2881f748a46",
|
||||
"d9c464d0dc78",
|
||||
"91e7f4ef9a77"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { SectionRepo } from '../worktree/workspace-list-types'
|
||||
import { sectionRepoColor } from '../worktree/workspace-filter-repos'
|
||||
import { readMobileRepoIcon, type MobileHostRepoIcon } from './host-screen-reply-schema'
|
||||
|
||||
/** The colour and icon a repo's header and rows draw. */
|
||||
export type RepoLook = { color: string; icon: MobileHostRepoIcon | null }
|
||||
|
||||
/** Decoded once per catalog, so a row's props keep their identity across polls. */
|
||||
export function buildRepoLookById(
|
||||
repoById: ReadonlyMap<string, SectionRepo>
|
||||
): ReadonlyMap<string, RepoLook> {
|
||||
return new Map(
|
||||
[...repoById].map(([id, repo]) => [
|
||||
id,
|
||||
{ color: sectionRepoColor(repo), icon: readMobileRepoIcon(repo.repoIcon) }
|
||||
])
|
||||
)
|
||||
}
|
||||
@@ -95,7 +95,7 @@ export function HostScreenOverlays({ controller }: { controller: HostScreenContr
|
||||
<Text style={styles.filterSectionLabel}>Repositories</Text>
|
||||
<View style={styles.filterGroup}>
|
||||
{controller.sectionsResult.uniqueRepos.map((repo, i) => (
|
||||
<View key={repo.id}>
|
||||
<View key={repo.key}>
|
||||
{i > 0 && <View style={styles.filterSeparator} />}
|
||||
<Pressable
|
||||
style={styles.filterRow}
|
||||
|
||||
@@ -30,6 +30,17 @@ const repoIconBranches = {
|
||||
label: salvagedOptional('label', z.string())
|
||||
})
|
||||
} satisfies Readonly<Record<RepoIcon['type'], z.ZodType>>
|
||||
const mobileRepoIconSchema = z.union([
|
||||
repoIconBranches.lucide,
|
||||
repoIconBranches.emoji,
|
||||
repoIconBranches.image
|
||||
])
|
||||
|
||||
/** A catalog repo's icon as MobileRepoIcon draws it; null for an arm this build cannot read. */
|
||||
export function readMobileRepoIcon(value: unknown): MobileHostRepoIcon | null {
|
||||
const parsed = mobileRepoIconSchema.safeParse(value)
|
||||
return parsed.success ? parsed.data : null
|
||||
}
|
||||
|
||||
// What the host screen reads to label its rows and to mirror the desktop's workspace view store.
|
||||
// Checked against src/main/runtime/rpc/methods/repo.ts:29, ssh.ts:55, host-capabilities.ts:8 and
|
||||
@@ -37,12 +48,12 @@ const repoIconBranches = {
|
||||
// PersistedUIState's workspace-view subset in mobile/src/worktree/workspace-view-settings.ts:14.
|
||||
|
||||
/**
|
||||
* The host's repo catalog, narrowed to what the label maps are built from.
|
||||
* The host's repo catalog, narrowed to what the host-label maps and the repo cache are built from.
|
||||
*
|
||||
* `id` and `displayName` are required and a row without either drops: `displayName` is the key of
|
||||
* four Maps and the argument `repoColor` hashes with `name.charCodeAt` — a row without one was a
|
||||
* TypeError that took the whole metadata refresh with it — and `id` is the Map value the workspace
|
||||
* rows resolve their host through.
|
||||
* `id` and `displayName` are required and a row without either drops: `displayName` is what the
|
||||
* new-workspace picker shows from the repo cache, and `id` is the Map key the workspace rows
|
||||
* resolve their host through. `path` labels an older desktop's repo sections (the newer one serves
|
||||
* project-sections-operations.ts instead).
|
||||
*
|
||||
* `badgeColor` sits behind main's own `||` fallback to `repoColor`, so a salvaged member draws the
|
||||
* same swatch. `repoIcon` is declared as the three arms MobileRepoIcon renders, and an arm this
|
||||
@@ -64,11 +75,9 @@ export const hostRepoCatalogSchema = z
|
||||
z.looseObject({
|
||||
id: z.string(),
|
||||
displayName: z.string(),
|
||||
path: salvagedOptional('path', z.string()),
|
||||
badgeColor: salvagedOptional('badgeColor', z.string()),
|
||||
repoIcon: salvagedOptional(
|
||||
'repoIcon',
|
||||
z.union([repoIconBranches.lucide, repoIconBranches.emoji, repoIconBranches.image])
|
||||
),
|
||||
repoIcon: salvagedOptional('repoIcon', mobileRepoIconSchema),
|
||||
connectionId: salvagedOptional('connectionId', z.string().nullable()),
|
||||
executionHostId: salvagedOptional('executionHostId', z.string().nullable())
|
||||
})
|
||||
@@ -184,6 +193,8 @@ export const hostViewSettingsSchema = z
|
||||
*/
|
||||
export const hostScreenUnreadReplySchema = z.unknown()
|
||||
|
||||
export type HostRepoCatalogRow = z.output<typeof hostRepoCatalogSchema>[number]
|
||||
|
||||
/** One decoded catalog icon: the members MobileRepoIcon reads, with the rest passed through. */
|
||||
export type MobileHostRepoIcon = NonNullable<
|
||||
z.output<typeof hostRepoCatalogSchema>[number]['repoIcon']
|
||||
|
||||
@@ -48,6 +48,7 @@ function screenState(clientRef: ClientRef, applied: boolean[]) {
|
||||
[
|
||||
'setActionError',
|
||||
'setCatalogError',
|
||||
'setDesktopRepos',
|
||||
'setError',
|
||||
'setHostLabelById',
|
||||
'setHostHealthById',
|
||||
@@ -57,9 +58,8 @@ function screenState(clientRef: ClientRef, applied: boolean[]) {
|
||||
'setLastKnownWorktrees',
|
||||
'setOptimisticActiveWorktreeIdentity',
|
||||
'setPinnedIds',
|
||||
'setRepoColorsByName',
|
||||
'setRepoHostIdByRepoId',
|
||||
'setRepoIconsByName',
|
||||
'setServedProjectSections',
|
||||
'setSleptIds',
|
||||
'setWorktrees',
|
||||
'setWorktreesLoaded'
|
||||
|
||||
@@ -60,7 +60,7 @@ function listWith(fields: { actionError: string; setActionError: (value: string)
|
||||
relayRecovery: { pairingRejected: false },
|
||||
routeNotice: '',
|
||||
router: { push: () => {} },
|
||||
sectionsResult: { rawSections: [], sections: [], uniqueRepoColors: new Map() },
|
||||
sectionsResult: { rawSections: [], sections: [], repoById: new Map() },
|
||||
setDismissedNotice: () => {},
|
||||
settings: { activeFilterCount: 0 },
|
||||
state: {
|
||||
@@ -69,7 +69,6 @@ function listWith(fields: { actionError: string; setActionError: (value: string)
|
||||
catalogError: null,
|
||||
collapsedGroups: new Set(),
|
||||
groupMode: 'none',
|
||||
repoIconsByName: new Map(),
|
||||
search: '',
|
||||
showSearch: false,
|
||||
worktreesLoaded: true
|
||||
|
||||
@@ -68,7 +68,7 @@ function worktree(id: string, overrides: Partial<Worktree> = {}): Worktree {
|
||||
}
|
||||
|
||||
const noop = () => {}
|
||||
const stable = { ref: { current: null }, repoColors: new Map(), repoIcons: new Map() }
|
||||
const stable = { ref: { current: null }, repoById: new Map() }
|
||||
|
||||
function listProps(worktrees: Worktree[]) {
|
||||
// Each poll re-parses the payload, so rows are fresh objects even when nothing changed.
|
||||
@@ -91,13 +91,12 @@ function listProps(worktrees: Worktree[]) {
|
||||
now: 1_000,
|
||||
reconnectAttempts: 0,
|
||||
relayRecovery: { pairingRejected: false },
|
||||
sectionsResult: { rawSections: sections, sections, uniqueRepoColors: stable.repoColors },
|
||||
sectionsResult: { rawSections: sections, sections, repoById: stable.repoById },
|
||||
settings: { activeFilterCount: 0, toggleWorktreeLineage: noop },
|
||||
state: {
|
||||
actionError: '',
|
||||
setActionTarget: noop,
|
||||
groupMode: 'none',
|
||||
repoIconsByName: stable.repoIcons,
|
||||
search: '',
|
||||
showSearch: false,
|
||||
worktreesLoaded: true
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useMemo } from 'react'
|
||||
import { Platform, Pressable, RefreshControl, SectionList, Text, View } from 'react-native'
|
||||
import { ChevronDown, ChevronRight, Pin } from 'lucide-react-native'
|
||||
import { AuthFailedBanner } from '../components/AuthFailedBanner'
|
||||
@@ -13,6 +14,7 @@ import { HostWorkspaceListStates } from '../worktree/host-workspace-list-states'
|
||||
import { getWorktreeStatus } from '../worktree/workspace-list-sections'
|
||||
import { repoColor } from '../worktree/repo-color'
|
||||
import { hostScreenStyles as styles } from './host-screen-styles'
|
||||
import { buildRepoLookById } from './host-repo-looks'
|
||||
import type { HostScreenController } from './use-host-screen-controller'
|
||||
|
||||
export function HostWorkspaceList({ controller }: { controller: HostScreenController }) {
|
||||
@@ -40,7 +42,8 @@ export function HostWorkspaceList({ controller }: { controller: HostScreenContro
|
||||
settings,
|
||||
state
|
||||
} = controller
|
||||
const { rawSections, sections, uniqueRepoColors } = sectionsResult
|
||||
const { rawSections, sections, repoById } = sectionsResult
|
||||
const repoLookById = useMemo(() => buildRepoLookById(repoById), [repoById])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -140,10 +143,11 @@ export function HostWorkspaceList({ controller }: { controller: HostScreenContro
|
||||
const isCollapsed = state.collapsedGroups.has(section.key)
|
||||
const rawSection = rawSections.find((s) => s.key === section.key)
|
||||
const count = rawSection?.data.length ?? 0
|
||||
const repoSectionColor =
|
||||
state.groupMode === 'repo' ? uniqueRepoColors.get(section.title) : null
|
||||
const repoSectionIcon =
|
||||
state.groupMode === 'repo' ? state.repoIconsByName.get(section.title) : null
|
||||
// Why: the desktop's header draws its anchor repo's colour and icon.
|
||||
const repoSectionLook =
|
||||
state.groupMode === 'repo' && section.repo ? repoLookById.get(section.repo.id) : null
|
||||
const repoSectionColor = repoSectionLook?.color ?? null
|
||||
const repoSectionIcon = repoSectionLook?.icon ?? null
|
||||
return (
|
||||
<Pressable
|
||||
style={styles.sectionHeader}
|
||||
@@ -187,8 +191,8 @@ export function HostWorkspaceList({ controller }: { controller: HostScreenContro
|
||||
isReadOnly={isReadOnly}
|
||||
now={now}
|
||||
status={getWorktreeStatus(item)}
|
||||
repoColor={uniqueRepoColors.get(item.repo) ?? repoColor(item.repo)}
|
||||
repoIcon={state.repoIconsByName.get(item.repo) ?? null}
|
||||
repoColor={repoLookById.get(item.repoId)?.color ?? repoColor(item.repo)}
|
||||
repoIcon={repoLookById.get(item.repoId)?.icon ?? null}
|
||||
hideRepo={state.groupMode === 'repo'}
|
||||
onPress={actions.openWorktreeSession}
|
||||
onLongPress={
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { createElement } from 'react'
|
||||
import { act, create } from 'react-test-renderer'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const request = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('../worktree/project-sections-operations', () => ({
|
||||
projectSectionsRead: { request },
|
||||
readServedProjectSections: () => ({ kind: 'answered', value: null })
|
||||
}))
|
||||
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import { useHostProjectSections } from './use-host-project-sections'
|
||||
|
||||
function clientDouble(): RpcClient {
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the hook only compares the client by identity and hands it to the mocked read.
|
||||
return {} as RpcClient
|
||||
}
|
||||
|
||||
describe('useHostProjectSections', () => {
|
||||
it('fetches the catalog at once when the screen moves to another host client', async () => {
|
||||
request.mockResolvedValue({ ok: true })
|
||||
const clientRef: { current: RpcClient | null } = { current: null }
|
||||
let refresh: (options?: { force?: boolean }) => Promise<void> = async () => {}
|
||||
function Probe({ client, hostId }: { client: RpcClient; hostId: string }) {
|
||||
refresh = useHostProjectSections({
|
||||
client,
|
||||
connState: 'connected',
|
||||
hostId,
|
||||
state: { clientRef, setServedProjectSections: () => {} }
|
||||
})
|
||||
return null
|
||||
}
|
||||
const first = clientDouble()
|
||||
const second = clientDouble()
|
||||
clientRef.current = first
|
||||
let tree!: ReturnType<typeof create>
|
||||
act(() => {
|
||||
tree = create(createElement(Probe, { client: first, hostId: 'a' }))
|
||||
})
|
||||
await act(async () => refresh())
|
||||
await act(async () => refresh())
|
||||
expect(request).toHaveBeenCalledTimes(1)
|
||||
|
||||
clientRef.current = second
|
||||
act(() => tree.update(createElement(Probe, { client: second, hostId: 'b' })))
|
||||
await act(async () => refresh())
|
||||
expect(request).toHaveBeenCalledTimes(2)
|
||||
expect(request).toHaveBeenLastCalledWith(second)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useCallback, useRef } from 'react'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import type { ConnectionState } from '../transport/types'
|
||||
import {
|
||||
projectSectionsRead,
|
||||
readServedProjectSections
|
||||
} from '../worktree/project-sections-operations'
|
||||
import type { HostScreenState } from './use-host-screen-state'
|
||||
|
||||
const PROJECT_SECTIONS_REFRESH_MS = 60_000
|
||||
|
||||
/**
|
||||
* The desktop sidebar's Project catalog, refreshed with repo metadata but as its own read, so a
|
||||
* slow or missing answer never holds the repo labels back.
|
||||
*/
|
||||
export function useHostProjectSections(args: {
|
||||
client: RpcClient | null
|
||||
connState: ConnectionState
|
||||
hostId: string | undefined
|
||||
state: Pick<HostScreenState, 'clientRef' | 'setServedProjectSections'>
|
||||
}): (options?: { force?: boolean }) => Promise<void> {
|
||||
const { client, connState, hostId, state } = args
|
||||
const { clientRef, setServedProjectSections } = state
|
||||
const inFlight = useRef(new WeakSet<RpcClient>())
|
||||
// Per client, so a reused screen for another host fetches its catalog at once.
|
||||
const fetchedAt = useRef(new WeakMap<RpcClient, number>())
|
||||
|
||||
return useCallback(
|
||||
async (options: { force?: boolean } = {}) => {
|
||||
if (!client || connState !== 'connected' || !hostId || inFlight.current.has(client)) {
|
||||
return
|
||||
}
|
||||
if (
|
||||
!options.force &&
|
||||
Date.now() - (fetchedAt.current.get(client) ?? 0) < PROJECT_SECTIONS_REFRESH_MS
|
||||
) {
|
||||
return
|
||||
}
|
||||
inFlight.current.add(client)
|
||||
try {
|
||||
const reply = await projectSectionsRead.request(client).catch(() => null)
|
||||
if (clientRef.current !== client) {
|
||||
return
|
||||
}
|
||||
const sections = readServedProjectSections(reply)
|
||||
if (sections.kind === 'answered') {
|
||||
fetchedAt.current.set(client, Date.now())
|
||||
setServedProjectSections(sections.value)
|
||||
}
|
||||
} finally {
|
||||
inFlight.current.delete(client)
|
||||
}
|
||||
},
|
||||
[client, clientRef, connState, hostId, setServedProjectSections]
|
||||
)
|
||||
}
|
||||
@@ -5,12 +5,13 @@ import { setCachedRepos } from '../cache/repo-cache'
|
||||
import type { RpcAcceptedResult } from '../transport/rpc-accepted-result'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import type { ConnectionState, RpcResponse } from '../transport/types'
|
||||
import { repoColor } from '../worktree/repo-color'
|
||||
import {
|
||||
buildHostLabelById,
|
||||
buildRepoHostIdByRepoId
|
||||
} from '../worktree/worktree-host-context-labels'
|
||||
import { buildSshHostHealthById } from '../worktree/worktree-host-health'
|
||||
import type { SectionRepo } from '../worktree/workspace-list-types'
|
||||
import type { HostRepoCatalogRow } from './host-screen-reply-schema'
|
||||
import {
|
||||
hostPlatformRead,
|
||||
hostRepoCatalogRead,
|
||||
@@ -41,6 +42,18 @@ function acceptedMetadata<Value>(
|
||||
return verdict.accepted ? verdict.value : null
|
||||
}
|
||||
|
||||
/** A desktop repo.list row as Project sections read it, for a desktop that serves no catalog. */
|
||||
function desktopSectionRepo(repo: HostRepoCatalogRow): SectionRepo {
|
||||
return {
|
||||
id: repo.id,
|
||||
displayName: repo.displayName,
|
||||
path: repo.path ?? repo.id,
|
||||
executionHostId: getRepoExecutionHostId(repo),
|
||||
...(repo.badgeColor ? { badgeColor: repo.badgeColor } : {}),
|
||||
...(repo.repoIcon ? { repoIcon: repo.repoIcon } : {})
|
||||
}
|
||||
}
|
||||
|
||||
function readHostSettingOverrides(result: unknown): unknown {
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary.
|
||||
return (result as { hostSettingOverrides?: unknown } | null)?.hostSettingOverrides
|
||||
@@ -60,11 +73,9 @@ export function useHostRepoMetadata(args: {
|
||||
repoMetadataFetchedAtRef,
|
||||
setHostLabelById,
|
||||
setHostHealthById,
|
||||
setDesktopRepos,
|
||||
setHostPlatform,
|
||||
setRepoColorsByName,
|
||||
setRepoHostIdByRepoId,
|
||||
setRepoIconsByName,
|
||||
setRepoIdsByName
|
||||
setRepoHostIdByRepoId
|
||||
} = state
|
||||
|
||||
const fetchRepoMetadata = useCallback(
|
||||
@@ -101,22 +112,7 @@ export function useHostRepoMetadata(args: {
|
||||
const catalog = repos.value
|
||||
repoMetadataFetchedAtRef.current = Date.now()
|
||||
setCachedRepos(requestHostId, catalog)
|
||||
setRepoColorsByName(
|
||||
new Map(
|
||||
catalog.map((repo) => [
|
||||
repo.displayName,
|
||||
repo.badgeColor || repoColor(repo.displayName)
|
||||
])
|
||||
)
|
||||
)
|
||||
setRepoIconsByName(
|
||||
new Map(
|
||||
catalog.flatMap((repo) =>
|
||||
repo.repoIcon ? [[repo.displayName, repo.repoIcon] as const] : []
|
||||
)
|
||||
)
|
||||
)
|
||||
setRepoIdsByName(new Map(catalog.map((repo) => [repo.displayName, repo.id])))
|
||||
setDesktopRepos(catalog.map(desktopSectionRepo))
|
||||
setRepoHostIdByRepoId(buildRepoHostIdByRepoId(catalog))
|
||||
// Why: rows name their host when the list spans hosts or an SSH host may be unhealthy,
|
||||
// so a local-only catalog never pays for the label lookups. Counted over repos, not the
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useLocalSearchParams, usePathname } from 'expo-router'
|
||||
import { useRouteHandoff } from '../navigation/route-handoff'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
@@ -16,7 +16,9 @@ import {
|
||||
} from '../transport/client-context-connection-metrics'
|
||||
import { useWorkspaceSections } from '../worktree/use-workspace-sections'
|
||||
import { useHostDisplayWorktrees } from './use-host-display-worktrees'
|
||||
import { useHostRepoMetadata } from './use-host-repo-metadata'
|
||||
import { buildProjectSectionCatalog } from '../worktree/project-section-catalog'
|
||||
import { useHostProjectSections } from './use-host-project-sections'
|
||||
import { useHostRepoMetadata, type FetchHostRepoMetadata } from './use-host-repo-metadata'
|
||||
import { useHostScreenIdentity } from './use-host-screen-identity'
|
||||
import { useHostScreenState } from './use-host-screen-state'
|
||||
import { useHostViewSettings } from './use-host-view-settings'
|
||||
@@ -72,7 +74,15 @@ export function useHostScreenController({
|
||||
)
|
||||
|
||||
useHostScreenIdentity({ client, hostId, state })
|
||||
const fetchRepoMetadata = useHostRepoMetadata({ client, connState, hostId, state })
|
||||
const fetchRepoMetadataOnly = useHostRepoMetadata({ client, connState, hostId, state })
|
||||
const fetchProjectSections = useHostProjectSections({ client, connState, hostId, state })
|
||||
const fetchRepoMetadata = useCallback<FetchHostRepoMetadata>(
|
||||
(options) => {
|
||||
void fetchProjectSections(options)
|
||||
return fetchRepoMetadataOnly(options)
|
||||
},
|
||||
[fetchProjectSections, fetchRepoMetadataOnly]
|
||||
)
|
||||
const catalog = useHostWorktreeCatalog({
|
||||
client,
|
||||
connState,
|
||||
@@ -104,6 +114,15 @@ export function useHostScreenController({
|
||||
const showNewWorktree = resolvedRouteActionState.showNewWorktree
|
||||
|
||||
const displayWorktrees = useHostDisplayWorktrees(connState, state, hostId)
|
||||
const projectCatalog = useMemo(
|
||||
() =>
|
||||
buildProjectSectionCatalog(
|
||||
state.servedProjectSections,
|
||||
state.desktopRepos,
|
||||
state.visibleHostIds
|
||||
),
|
||||
[state.servedProjectSections, state.desktopRepos, state.visibleHostIds]
|
||||
)
|
||||
const sectionsResult = useWorkspaceSections({
|
||||
displayWorktrees,
|
||||
sortMode: state.sortMode,
|
||||
@@ -112,8 +131,8 @@ export function useHostScreenController({
|
||||
groupMode: state.groupMode,
|
||||
pinnedIds: state.pinnedIds,
|
||||
showPinnedInGroups: state.showPinnedInGroups,
|
||||
repoIdsByName: state.repoIdsByName,
|
||||
repoColorsByName: state.repoColorsByName,
|
||||
projectCatalog,
|
||||
filterCatalogRepos: state.servedProjectSections?.repos ?? state.desktopRepos,
|
||||
collapsedGroups: state.collapsedGroups,
|
||||
workspaceStatuses: state.workspaceStatuses
|
||||
})
|
||||
|
||||
@@ -18,15 +18,15 @@ export function useHostScreenIdentity(args: {
|
||||
setCatalogError,
|
||||
setError,
|
||||
setHostLabelById,
|
||||
setServedProjectSections,
|
||||
setDesktopRepos,
|
||||
setHostHealthById,
|
||||
setHostName,
|
||||
setHostPlatform,
|
||||
setHostStoredDescriptor,
|
||||
setLastKnownWorktrees,
|
||||
setPinnedIds,
|
||||
setRepoColorsByName,
|
||||
setRepoHostIdByRepoId,
|
||||
setRepoIconsByName,
|
||||
setShowPinnedInGroups,
|
||||
setWorktrees,
|
||||
setWorktreesLoaded
|
||||
@@ -59,10 +59,10 @@ export function useHostScreenIdentity(args: {
|
||||
setHostName('')
|
||||
setHostStoredDescriptor(null)
|
||||
setError('')
|
||||
setRepoColorsByName(new Map())
|
||||
setRepoIconsByName(new Map())
|
||||
setRepoHostIdByRepoId(new Map())
|
||||
setHostLabelById(new Map())
|
||||
setServedProjectSections(null)
|
||||
setDesktopRepos([])
|
||||
setHostHealthById(new Map())
|
||||
setHostPlatform(null)
|
||||
setShowPinnedInGroups(false)
|
||||
|
||||
@@ -13,7 +13,8 @@ import type {
|
||||
MobileViewState
|
||||
} from '../worktree/workspace-view-settings'
|
||||
import type { FilterState, Worktree } from '../worktree/workspace-list-sections'
|
||||
import type { MobileHostRepoIcon } from './host-screen-reply-schema'
|
||||
import type { ServedProjectSections } from '../worktree/project-sections-operations'
|
||||
import type { SectionRepo } from '../worktree/workspace-list-types'
|
||||
import { NO_SERVER_WORKSPACES, type ServerWorkspaces } from '../worktree/server-workspaces'
|
||||
|
||||
export function useHostScreenState(hostId: string | undefined, action: string | undefined) {
|
||||
@@ -39,8 +40,6 @@ export function useHostScreenState(hostId: string | undefined, action: string |
|
||||
const [optimisticActiveWorktreeIdentity, setOptimisticActiveWorktreeIdentity] = useState<
|
||||
string | null
|
||||
>(null)
|
||||
const [repoColorsByName, setRepoColorsByName] = useState<Map<string, string>>(new Map())
|
||||
const [repoIconsByName, setRepoIconsByName] = useState<Map<string, MobileHostRepoIcon>>(new Map())
|
||||
const [hostName, setHostName] = useState('')
|
||||
// The stored name identity, loaded with the name so the header can label an offline host.
|
||||
const [hostStoredDescriptor, setHostStoredDescriptor] = useState<{
|
||||
@@ -66,8 +65,12 @@ export function useHostScreenState(hostId: string | undefined, action: string |
|
||||
const [workspaceStatuses, setWorkspaceStatuses] = useState<readonly WorkspaceStatusDefinition[]>(
|
||||
DEFAULT_MOBILE_WORKSPACE_STATUSES
|
||||
)
|
||||
// displayName → repo id: filters key on repo id, but section headers/rows key on displayName, so bridge the two.
|
||||
const [repoIdsByName, setRepoIdsByName] = useState<Map<string, string>>(new Map())
|
||||
// The desktop sidebar's Project grouping inputs; null for a desktop that serves none.
|
||||
const [servedProjectSections, setServedProjectSections] = useState<ServedProjectSections | null>(
|
||||
null
|
||||
)
|
||||
// The desktop's own repos from repo.list: an older desktop's rows group by these.
|
||||
const [desktopRepos, setDesktopRepos] = useState<readonly SectionRepo[]>([])
|
||||
// Host-label inputs for rows: repo → host, SSH/override labels, and the host's own platform.
|
||||
const [repoHostIdByRepoId, setRepoHostIdByRepoId] = useState<Map<string, ExecutionHostId>>(
|
||||
new Map()
|
||||
@@ -133,10 +136,9 @@ export function useHostScreenState(hostId: string | undefined, action: string |
|
||||
newWorktreeModalVisibleRef,
|
||||
optimisticActiveWorktreeIdentity,
|
||||
pinnedIds,
|
||||
repoColorsByName,
|
||||
repoHostIdByRepoId,
|
||||
repoIconsByName,
|
||||
repoIdsByName,
|
||||
servedProjectSections,
|
||||
desktopRepos,
|
||||
repoMetadataFetchedAtRef,
|
||||
routeActionState,
|
||||
search,
|
||||
@@ -159,10 +161,9 @@ export function useHostScreenState(hostId: string | undefined, action: string |
|
||||
setLastKnownWorktrees,
|
||||
setOptimisticActiveWorktreeIdentity,
|
||||
setPinnedIds,
|
||||
setRepoColorsByName,
|
||||
setRepoHostIdByRepoId,
|
||||
setRepoIconsByName,
|
||||
setRepoIdsByName,
|
||||
setServedProjectSections,
|
||||
setDesktopRepos,
|
||||
setRouteActionState,
|
||||
setSearch,
|
||||
setServerNotice,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildProjectSectionCatalog } from './project-section-catalog'
|
||||
import type { ServedProjectSections } from './project-sections-operations'
|
||||
|
||||
const uploaded = { type: 'image', src: 'data:image/png;base64,AAAA' }
|
||||
|
||||
type ServedRepo = ServedProjectSections['repos'][number]
|
||||
|
||||
function repo(
|
||||
id: string,
|
||||
executionHostId?: ServedRepo['executionHostId'],
|
||||
repoIcon?: unknown
|
||||
): ServedRepo {
|
||||
return {
|
||||
id,
|
||||
displayName: 'orca',
|
||||
path: `/repos/${id}`,
|
||||
badgeColor: undefined,
|
||||
addedAt: undefined,
|
||||
connectionId: undefined,
|
||||
executionHostId,
|
||||
...(repoIcon ? { repoIcon } : {})
|
||||
}
|
||||
}
|
||||
|
||||
describe('buildProjectSectionCatalog', () => {
|
||||
it("restores an uploaded icon from the desktop's repo.list only for the same host's repo", () => {
|
||||
const served: ServedProjectSections = {
|
||||
repos: [repo('r1'), repo('r1', 'runtime:m4air')],
|
||||
projects: [],
|
||||
setups: [],
|
||||
projectOrderBy: 'manual'
|
||||
}
|
||||
const catalog = buildProjectSectionCatalog(served, [repo('r1', undefined, uploaded)], null)
|
||||
expect(catalog.repos.map((entry) => entry.repoIcon)).toEqual([uploaded, undefined])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
import { getRepoExecutionHostId, type ExecutionHostId } from '../../../src/shared/execution-host'
|
||||
import { getLogicalRepoOrderRankById } from '../../../src/shared/project-section-order'
|
||||
import type { ServedProjectSections } from './project-sections-operations'
|
||||
import { NO_PROJECT_SECTION_CATALOG, type ProjectSectionCatalog } from './workspace-list-sections'
|
||||
import type { SectionRepo } from './workspace-list-types'
|
||||
|
||||
/**
|
||||
* Project grouping's inputs: the desktop sidebar's own catalog when it serves one, else (an older
|
||||
* desktop) its own repos with no projects, so rows group by repo. Only shown hosts' repos open
|
||||
* empty sections; the grouping itself spans every host, as the desktop's does.
|
||||
*/
|
||||
export function buildProjectSectionCatalog(
|
||||
served: ServedProjectSections | null,
|
||||
desktopRepos: readonly SectionRepo[],
|
||||
visibleHostIds: ReadonlySet<ExecutionHostId> | null
|
||||
): ProjectSectionCatalog {
|
||||
const shown = (repos: readonly SectionRepo[]) =>
|
||||
visibleHostIds
|
||||
? repos.filter((repo) => visibleHostIds.has(getRepoExecutionHostId(repo)))
|
||||
: repos
|
||||
if (!served) {
|
||||
return { ...NO_PROJECT_SECTION_CATALOG, repos: shown(desktopRepos) }
|
||||
}
|
||||
// Why: the desktop withholds uploaded image icons from the snapshot; its repo.list still carries them.
|
||||
const iconKey = (repo: SectionRepo) => `${getRepoExecutionHostId(repo)}|${repo.id}`
|
||||
const iconByKey = new Map(desktopRepos.map((repo) => [iconKey(repo), repo.repoIcon]))
|
||||
const repos = served.repos.map((repo) => {
|
||||
const icon = repo.repoIcon ? undefined : iconByKey.get(iconKey(repo))
|
||||
return icon ? { ...repo, repoIcon: icon } : repo
|
||||
})
|
||||
return {
|
||||
repos: shown(repos),
|
||||
grouping: { projects: served.projects, projectHostSetups: served.setups },
|
||||
// Why: served repos come in the sidebar's header order, cross-host manual order included.
|
||||
repoOrder: getLogicalRepoOrderRankById(served.repos.map((repo) => repo.id)),
|
||||
projectOrderBy: served.projectOrderBy
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { z } from 'zod'
|
||||
import { MOBILE_PROJECT_SECTIONS_METHOD } from '../../../src/shared/mobile-project-sections-contract'
|
||||
import { normalizeExecutionHostId } from '../../../src/shared/execution-host'
|
||||
import type { ProjectHostSetupMethod } from '../../../src/shared/project-types'
|
||||
import type { RepoKind } from '../../../src/shared/repo-types'
|
||||
import type { ProjectOrderBy } from '../../../src/shared/ui-chrome-types'
|
||||
import { hostUnionArms, salvagedOptional, salvagingArray } from '../../../src/shared/zod-salvage'
|
||||
import { bindDeferredRpcOperation, defineRpcOperation } from '../transport/rpc-operation'
|
||||
import { rpcResultVariant } from '../transport/rpc-operation-result-reader'
|
||||
import { isMobileMethodUnavailableError } from '../transport/mobile-method-unavailable'
|
||||
import type { RpcResponse } from '../transport/types'
|
||||
import type { SectionRepo } from './workspace-list-types'
|
||||
|
||||
const SETUP_METHOD_ARMS = hostUnionArms<ProjectHostSetupMethod>({
|
||||
'legacy-repo': true,
|
||||
'imported-existing-folder': true,
|
||||
cloned: true,
|
||||
provisioned: true
|
||||
})
|
||||
const REPO_KIND_ARMS = hostUnionArms<RepoKind>({ git: true, folder: true })
|
||||
const PROJECT_ORDER_BY_ARMS = hostUnionArms<ProjectOrderBy>({ manual: true, recent: true })
|
||||
|
||||
// A host id the desktop spells; normalized, so a spelling this build cannot read drops.
|
||||
const hostIdSchema = z.string().transform((value, ctx) => {
|
||||
const hostId = normalizeExecutionHostId(value)
|
||||
if (!hostId) {
|
||||
ctx.addIssue({ code: 'custom', message: 'unknown host id' })
|
||||
return z.NEVER
|
||||
}
|
||||
return hostId
|
||||
})
|
||||
|
||||
const repoSchema = z.object({
|
||||
id: z.string(),
|
||||
displayName: z.string(),
|
||||
path: z.string(),
|
||||
badgeColor: salvagedOptional('badgeColor', z.string()),
|
||||
addedAt: salvagedOptional('addedAt', z.number()),
|
||||
connectionId: salvagedOptional('connectionId', z.string().nullable()),
|
||||
executionHostId: salvagedOptional('executionHostId', hostIdSchema),
|
||||
// Decoded where drawn (readMobileRepoIcon), so an arm this build lacks still keeps the row.
|
||||
repoIcon: z.unknown().optional()
|
||||
}) satisfies z.ZodType<SectionRepo, unknown>
|
||||
|
||||
const setupSchema = z.object({
|
||||
id: z.string(),
|
||||
projectId: z.string(),
|
||||
hostId: hostIdSchema,
|
||||
repoId: z.string(),
|
||||
path: z.string(),
|
||||
displayName: z.string(),
|
||||
// Why the fallback: only 'provisioned' changes grouping, so a newer arm groups as a checkout.
|
||||
setupMethod: z.enum(SETUP_METHOD_ARMS).catch('legacy-repo'),
|
||||
kind: salvagedOptional('kind', z.enum(REPO_KIND_ARMS)),
|
||||
connectionId: salvagedOptional('connectionId', z.string().nullable()),
|
||||
executionHostId: salvagedOptional('executionHostId', hostIdSchema)
|
||||
})
|
||||
|
||||
const catalogSchema = z.object({
|
||||
repos: salvagingArray(repoSchema),
|
||||
projects: salvagingArray(z.object({ id: z.string(), displayName: z.string() })),
|
||||
setups: salvagingArray(setupSchema),
|
||||
projectOrderBy: z.enum(PROJECT_ORDER_BY_ARMS).catch('manual')
|
||||
})
|
||||
|
||||
/** The desktop sidebar's Project grouping inputs, as this phone reads them. */
|
||||
export type ServedProjectSections = z.output<typeof catalogSchema>
|
||||
|
||||
/** The sidebar's Project grouping inputs; null until a desktop window has published them. */
|
||||
export const projectSectionsRead = bindDeferredRpcOperation(
|
||||
defineRpcOperation({
|
||||
name: 'mobileRelay.projectSections-or-skip',
|
||||
method: MOBILE_PROJECT_SECTIONS_METHOD,
|
||||
acceptance: 'success-result-or-skip',
|
||||
barrier: 'after-caller-barrier',
|
||||
read: rpcResultVariant(
|
||||
'project-sections',
|
||||
z.object({ catalog: catalogSchema.nullable() }).transform(({ catalog }) => catalog)
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* The desktop's sidebar catalog: absent (null) on a desktop that predates the read, and left
|
||||
* as it was after a transient failure, so a dropped reply never regroups the list.
|
||||
*/
|
||||
export function readServedProjectSections(
|
||||
reply: RpcResponse | null
|
||||
): { kind: 'answered'; value: ServedProjectSections | null } | { kind: 'kept' } {
|
||||
if (!reply) {
|
||||
return { kind: 'kept' }
|
||||
}
|
||||
if (!reply.ok) {
|
||||
return isMobileMethodUnavailableError(reply.error.code, reply.error.message)
|
||||
? { kind: 'answered', value: null }
|
||||
: { kind: 'kept' }
|
||||
}
|
||||
try {
|
||||
const verdict = projectSectionsRead.interpret(reply)
|
||||
return verdict.accepted ? { kind: 'answered', value: verdict.value } : { kind: 'kept' }
|
||||
} catch {
|
||||
return { kind: 'kept' }
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,15 @@ import { useMemo } from 'react'
|
||||
import type { WorkspaceStatusDefinition } from '../../../src/shared/worktree/types'
|
||||
import type { MobileGroupMode, MobileSortMode } from './workspace-view-settings'
|
||||
import {
|
||||
buildSectionRepoMap,
|
||||
buildSections,
|
||||
type FilterState,
|
||||
type ProjectSectionCatalog,
|
||||
type Section,
|
||||
type SectionRepo,
|
||||
type Worktree
|
||||
} from './workspace-list-sections'
|
||||
import { repoColor } from './repo-color'
|
||||
|
||||
export type WorkspaceSectionRepo = {
|
||||
name: string
|
||||
id: string
|
||||
color: string
|
||||
}
|
||||
import { buildFilterRepos, type WorkspaceSectionRepo } from './workspace-filter-repos'
|
||||
|
||||
export function useWorkspaceSections(args: {
|
||||
displayWorktrees: Worktree[]
|
||||
@@ -23,15 +20,17 @@ export function useWorkspaceSections(args: {
|
||||
groupMode: MobileGroupMode
|
||||
pinnedIds: Set<string>
|
||||
showPinnedInGroups: boolean
|
||||
repoIdsByName: Map<string, string>
|
||||
repoColorsByName: Map<string, string>
|
||||
projectCatalog: ProjectSectionCatalog
|
||||
/** Every catalog repo on every host, which is what the desktop's filter lists. */
|
||||
filterCatalogRepos: readonly SectionRepo[]
|
||||
collapsedGroups: Set<string>
|
||||
workspaceStatuses: readonly WorkspaceStatusDefinition[]
|
||||
}): {
|
||||
sections: Section[]
|
||||
rawSections: Section[]
|
||||
uniqueRepos: WorkspaceSectionRepo[]
|
||||
uniqueRepoColors: Map<string, string>
|
||||
/** Each row's repo by id, as the desktop's cards look theirs up for colour and icon. */
|
||||
repoById: ReadonlyMap<string, SectionRepo>
|
||||
} {
|
||||
const {
|
||||
displayWorktrees,
|
||||
@@ -41,28 +40,20 @@ export function useWorkspaceSections(args: {
|
||||
groupMode,
|
||||
pinnedIds,
|
||||
showPinnedInGroups,
|
||||
repoIdsByName,
|
||||
repoColorsByName,
|
||||
projectCatalog,
|
||||
filterCatalogRepos,
|
||||
collapsedGroups,
|
||||
workspaceStatuses
|
||||
} = args
|
||||
|
||||
const uniqueRepos = useMemo(() => {
|
||||
const repos = new Map<string, { id: string; color: string }>()
|
||||
for (const w of displayWorktrees) {
|
||||
if (!repos.has(w.repo)) {
|
||||
repos.set(w.repo, {
|
||||
id: repoIdsByName.get(w.repo) ?? w.repoId,
|
||||
color: repoColorsByName.get(w.repo) ?? repoColor(w.repo)
|
||||
})
|
||||
}
|
||||
}
|
||||
return [...repos.entries()].map(([name, { id, color }]) => ({ name, id, color }))
|
||||
}, [displayWorktrees, repoColorsByName, repoIdsByName])
|
||||
const uniqueRepos = useMemo(
|
||||
() => buildFilterRepos(filterCatalogRepos, displayWorktrees),
|
||||
[filterCatalogRepos, displayWorktrees]
|
||||
)
|
||||
|
||||
const uniqueRepoColors = useMemo(
|
||||
() => new Map(uniqueRepos.map((repo) => [repo.name, repo.color])),
|
||||
[uniqueRepos]
|
||||
const repoById = useMemo(
|
||||
() => buildSectionRepoMap(projectCatalog.repos, displayWorktrees),
|
||||
[projectCatalog.repos, displayWorktrees]
|
||||
)
|
||||
|
||||
const rawSections = useMemo(
|
||||
@@ -74,7 +65,7 @@ export function useWorkspaceSections(args: {
|
||||
search,
|
||||
groupMode,
|
||||
pinnedIds,
|
||||
repoIdsByName,
|
||||
projectCatalog,
|
||||
workspaceStatuses,
|
||||
collapsedGroups,
|
||||
showPinnedInGroups
|
||||
@@ -86,7 +77,7 @@ export function useWorkspaceSections(args: {
|
||||
search,
|
||||
groupMode,
|
||||
pinnedIds,
|
||||
repoIdsByName,
|
||||
projectCatalog,
|
||||
workspaceStatuses,
|
||||
collapsedGroups,
|
||||
showPinnedInGroups
|
||||
@@ -102,5 +93,5 @@ export function useWorkspaceSections(args: {
|
||||
[rawSections, collapsedGroups]
|
||||
)
|
||||
|
||||
return { sections, rawSections, uniqueRepos, uniqueRepoColors }
|
||||
return { sections, rawSections, uniqueRepos, repoById }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildFilterRepos } from './workspace-filter-repos'
|
||||
import { filterWorktrees, type Worktree } from './workspace-list-sections'
|
||||
|
||||
function row(worktreeId: string, repoId: string, hostId: Worktree['hostId']): Worktree {
|
||||
return {
|
||||
worktreeId,
|
||||
repoId,
|
||||
hostId,
|
||||
repo: 'orca',
|
||||
branch: 'feature',
|
||||
displayName: worktreeId,
|
||||
path: `/tmp/${worktreeId}`,
|
||||
liveTerminalCount: 0,
|
||||
hasAttachedPty: false,
|
||||
unread: false,
|
||||
isPinned: false,
|
||||
linkedPR: null
|
||||
}
|
||||
}
|
||||
|
||||
describe('the repo filter', () => {
|
||||
it("lists each host's same-named repo separately, by id, as the desktop's filter does", () => {
|
||||
const repos = buildFilterRepos(
|
||||
[
|
||||
{ id: 'local-orca', displayName: 'orca', path: '/Users/me/orca', badgeColor: '#111111' },
|
||||
{
|
||||
id: 'm4-orca',
|
||||
displayName: 'orca',
|
||||
path: '/Users/m4air/orca/orca',
|
||||
executionHostId: 'runtime:m4air'
|
||||
},
|
||||
{ id: 'assets', displayName: 'design-assets', path: '/Users/me/design-assets' }
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
expect(repos.map(({ key, id, name }) => ({ key, id, name }))).toEqual([
|
||||
{ key: 'local|local-orca', id: 'local-orca', name: 'orca' },
|
||||
{ key: 'runtime:m4air|m4-orca', id: 'm4-orca', name: 'orca' },
|
||||
{ key: 'local|assets', id: 'assets', name: 'design-assets' }
|
||||
])
|
||||
expect(repos[0]?.color).toBe('#111111')
|
||||
})
|
||||
|
||||
it('lists the repos the rows name until a catalog answers, still one per repo id', () => {
|
||||
const repos = buildFilterRepos(
|
||||
[],
|
||||
[row('a', 'local-orca', 'local'), row('b', 'm4-orca', 'runtime:m4air')]
|
||||
)
|
||||
|
||||
expect(repos.map((repo) => repo.id)).toEqual(['local-orca', 'm4-orca'])
|
||||
})
|
||||
|
||||
it('filters by the picked repo id, leaving the same-named repo on another host out', () => {
|
||||
const rows = [row('a', 'local-orca', 'local'), row('b', 'm4-orca', 'runtime:m4air')]
|
||||
|
||||
expect(
|
||||
filterWorktrees(
|
||||
rows,
|
||||
{ filterRepoIds: new Set(['m4-orca']), hideSleeping: false, hideDefaultBranch: false },
|
||||
''
|
||||
).map((w) => w.worktreeId)
|
||||
).toEqual(['b'])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,39 @@
|
||||
import { getRepoExecutionHostId } from '../../../src/shared/execution-host'
|
||||
import { repoColor } from './repo-color'
|
||||
import { buildSectionRepoMap, type SectionRepo, type Worktree } from './workspace-list-sections'
|
||||
|
||||
export type WorkspaceSectionRepo = {
|
||||
/** Unique per host: two hosts can each hold a repo of the same name. */
|
||||
key: string
|
||||
name: string
|
||||
id: string
|
||||
color: string
|
||||
}
|
||||
|
||||
/** A repo's badge colour, else the colour its name hashes to. */
|
||||
export function sectionRepoColor(repo: Pick<SectionRepo, 'badgeColor' | 'displayName'>): string {
|
||||
return (typeof repo.badgeColor === 'string' && repo.badgeColor) || repoColor(repo.displayName)
|
||||
}
|
||||
|
||||
/**
|
||||
* The repos the filter lists, as the desktop's project filter lists them: every catalog repo by
|
||||
* id, each host's separately. Before any catalog answers, the repos the rows name.
|
||||
*/
|
||||
export function buildFilterRepos(
|
||||
filterCatalogRepos: readonly SectionRepo[],
|
||||
worktrees: readonly Worktree[]
|
||||
): WorkspaceSectionRepo[] {
|
||||
const repos =
|
||||
filterCatalogRepos.length > 0
|
||||
? filterCatalogRepos
|
||||
: [...buildSectionRepoMap([], worktrees).values()]
|
||||
const seen = new Set<string>()
|
||||
return repos.flatMap((repo) => {
|
||||
const key = `${getRepoExecutionHostId(repo)}|${repo.id}`
|
||||
if (seen.has(key)) {
|
||||
return []
|
||||
}
|
||||
seen.add(key)
|
||||
return [{ key, id: repo.id, name: repo.displayName, color: sectionRepoColor(repo) }]
|
||||
})
|
||||
}
|
||||
@@ -18,8 +18,8 @@ export const WORKSPACE_SORT_OPTIONS: PickerOption<MobileSortMode>[] = [
|
||||
]
|
||||
|
||||
export const WORKSPACE_GROUP_OPTIONS: PickerOption<MobileGroupMode>[] = [
|
||||
{ value: 'none', label: 'No Grouping' },
|
||||
{ value: 'none', label: 'None' },
|
||||
{ value: 'workspaceStatus', label: 'Status' },
|
||||
{ value: 'repo', label: 'Repository' },
|
||||
{ value: 'repo', label: 'Project' },
|
||||
{ value: 'prStatus', label: 'PR Status' }
|
||||
]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { Worktree } from './workspace-list-sections'
|
||||
import { buildSections } from './workspace-list-sections'
|
||||
import { NO_PROJECT_SECTION_CATALOG, buildSections } from './workspace-list-sections'
|
||||
import { DEFAULT_MOBILE_WORKSPACE_STATUSES } from './mobile-workspace-statuses'
|
||||
|
||||
function worktree(overrides: Partial<Worktree> = {}): Worktree {
|
||||
@@ -41,7 +41,7 @@ describe('a pinned parent with an unpinned child', () => {
|
||||
'',
|
||||
'repo',
|
||||
new Set(),
|
||||
new Map(),
|
||||
NO_PROJECT_SECTION_CATALOG,
|
||||
DEFAULT_MOBILE_WORKSPACE_STATUSES,
|
||||
new Set(),
|
||||
showPinnedInGroups
|
||||
@@ -64,7 +64,7 @@ describe('a pinned parent with an unpinned child', () => {
|
||||
it('nests the child under its parent in both sections when the desktop setting is on', () => {
|
||||
expect(nesting(true)).toEqual([
|
||||
{ key: 'pinned', rows: nested },
|
||||
{ key: 'repo:orca', rows: nested }
|
||||
{ key: 'repo:repo-1', rows: nested }
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getRepoExecutionHostId } from '../../../src/shared/execution-host'
|
||||
import {
|
||||
PROJECT_SECTION_PARITY_HEADERS,
|
||||
PROJECT_SECTION_PARITY_ROWS,
|
||||
PROJECT_SECTION_PARITY_SERVED
|
||||
} from '../../../src/shared/project-section-parity.test-fixture'
|
||||
import { buildProjectSectionCatalog } from './project-section-catalog'
|
||||
import {
|
||||
projectSectionsRead,
|
||||
readServedProjectSections,
|
||||
type ServedProjectSections
|
||||
} from './project-sections-operations'
|
||||
import { buildSections, type Worktree } from './workspace-list-sections'
|
||||
|
||||
// The desktop half lives in src/renderer/src/components/sidebar/
|
||||
// worktree-list-groups-mobile-parity.test.ts: buildRows renders these headers, and main serves
|
||||
// PROJECT_SECTION_PARITY_SERVED for the same host answers.
|
||||
|
||||
function served(projectOrderBy: 'manual' | 'recent'): ServedProjectSections | null {
|
||||
const verdict = projectSectionsRead.interpret({
|
||||
id: 'sections',
|
||||
ok: true,
|
||||
result: { catalog: { ...PROJECT_SECTION_PARITY_SERVED, projectOrderBy } }
|
||||
})
|
||||
return verdict.accepted ? verdict.value : null
|
||||
}
|
||||
|
||||
function rows(): Worktree[] {
|
||||
const repoById = new Map(PROJECT_SECTION_PARITY_SERVED.repos.map((repo) => [repo.id, repo]))
|
||||
return PROJECT_SECTION_PARITY_ROWS.flatMap((row) => {
|
||||
const repo = repoById.get(row.repoId)
|
||||
return repo
|
||||
? [
|
||||
{
|
||||
worktreeId: row.id,
|
||||
repoId: row.repoId,
|
||||
hostId: getRepoExecutionHostId(repo),
|
||||
repo: repo.displayName,
|
||||
branch: 'feature',
|
||||
displayName: row.id,
|
||||
path: `${repo.path}-${row.id}`,
|
||||
lastActivityAt: row.lastActivityAt,
|
||||
liveTerminalCount: 0,
|
||||
hasAttachedPty: false,
|
||||
unread: false,
|
||||
isPinned: false,
|
||||
linkedPR: null
|
||||
}
|
||||
]
|
||||
: []
|
||||
})
|
||||
}
|
||||
|
||||
function headers(catalog: ReturnType<typeof buildProjectSectionCatalog>) {
|
||||
return buildSections(
|
||||
rows(),
|
||||
'recent',
|
||||
{ filterRepoIds: new Set(), hideSleeping: false, hideDefaultBranch: false },
|
||||
'',
|
||||
'repo',
|
||||
new Set(),
|
||||
catalog
|
||||
).map((section) => ({ key: section.key, label: section.title }))
|
||||
}
|
||||
|
||||
describe('Project grouping on the phone', () => {
|
||||
it.each(['recent', 'manual'] as const)(
|
||||
"renders the desktop sidebar's %s headers from the catalog the desktop serves",
|
||||
(projectOrderBy) => {
|
||||
expect(rows()).toHaveLength(PROJECT_SECTION_PARITY_ROWS.length)
|
||||
expect(headers(buildProjectSectionCatalog(served(projectOrderBy), [], null))).toEqual(
|
||||
PROJECT_SECTION_PARITY_HEADERS[projectOrderBy]
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
it("groups by repo from an older desktop's own repos when it serves no catalog", () => {
|
||||
const desktopRepos = PROJECT_SECTION_PARITY_SERVED.repos.filter(
|
||||
(repo) => repo.executionHostId === 'local'
|
||||
)
|
||||
|
||||
expect(headers(buildProjectSectionCatalog(null, desktopRepos, null))).toEqual([
|
||||
{ key: 'repo:m4-orca-two', label: 'orca' },
|
||||
{ key: 'repo:win-orca', label: 'orca' },
|
||||
{ key: 'repo:local-workspace', label: 'Users/jinwoo/orca/workspaces/orca/orca' },
|
||||
{ key: 'repo:local-stably', label: 'Users/jinwoo/stably/orca' },
|
||||
{ key: 'repo:m4-orca', label: 'orca' },
|
||||
{ key: 'repo:local-assets', label: 'design-assets' }
|
||||
])
|
||||
})
|
||||
|
||||
it('drops a served setup whose host id this build cannot read, keeping the rest', () => {
|
||||
const verdict = projectSectionsRead.interpret({
|
||||
id: 'sections',
|
||||
ok: true,
|
||||
result: {
|
||||
catalog: {
|
||||
...PROJECT_SECTION_PARITY_SERVED,
|
||||
setups: [
|
||||
{ ...PROJECT_SECTION_PARITY_SERVED.setups[0], hostId: 'nonsense' },
|
||||
...PROJECT_SECTION_PARITY_SERVED.setups.slice(1)
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
expect(verdict.accepted && verdict.value?.setups).toHaveLength(
|
||||
PROJECT_SECTION_PARITY_SERVED.setups.length - 1
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('readServedProjectSections', () => {
|
||||
it('reads a served catalog, and a desktop with no published window as none', () => {
|
||||
expect(
|
||||
readServedProjectSections({
|
||||
id: 'sections',
|
||||
ok: true,
|
||||
result: { catalog: PROJECT_SECTION_PARITY_SERVED }
|
||||
})
|
||||
).toMatchObject({ kind: 'answered', value: { repos: expect.any(Array) } })
|
||||
expect(
|
||||
readServedProjectSections({ id: 'sections', ok: true, result: { catalog: null } })
|
||||
).toEqual({ kind: 'answered', value: null })
|
||||
})
|
||||
|
||||
it.each(['forbidden', 'method_not_found'])(
|
||||
'reads an older desktop that answers %s as serving none',
|
||||
(code) => {
|
||||
expect(
|
||||
readServedProjectSections({ id: 'sections', ok: false, error: { code, message: code } })
|
||||
).toEqual({ kind: 'answered', value: null })
|
||||
}
|
||||
)
|
||||
|
||||
it('keeps the last catalog after a timeout or a dropped reply', () => {
|
||||
expect(
|
||||
readServedProjectSections({
|
||||
id: 'sections',
|
||||
ok: false,
|
||||
error: { code: 'timeout', message: 'timed out' }
|
||||
})
|
||||
).toEqual({ kind: 'kept' })
|
||||
expect(readServedProjectSections(null)).toEqual({ kind: 'kept' })
|
||||
})
|
||||
})
|
||||
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import type { Worktree } from './workspace-list-sections'
|
||||
import {
|
||||
CREATE_GRACE_MS,
|
||||
NO_PROJECT_SECTION_CATALOG,
|
||||
buildSections,
|
||||
filterWorktrees,
|
||||
getWorktreeStatus,
|
||||
@@ -32,8 +33,16 @@ function worktree(overrides: Partial<Worktree> = {}): Worktree {
|
||||
}
|
||||
}
|
||||
|
||||
// A catalog of [displayName, id] repos with no project rows, as an old desktop answers.
|
||||
function catalogOf(repos: readonly (readonly [string, string])[]) {
|
||||
return {
|
||||
...NO_PROJECT_SECTION_CATALOG,
|
||||
repos: repos.map(([displayName, id]) => ({ id, displayName, path: `/repos/${id}` }))
|
||||
}
|
||||
}
|
||||
|
||||
function withoutSectionListKeys(sections: ReturnType<typeof buildSections>) {
|
||||
return sections.map((section) => ({
|
||||
return sections.map(({ repo: _repo, ...section }) => ({
|
||||
...section,
|
||||
data: section.data.map(
|
||||
({
|
||||
@@ -293,7 +302,7 @@ describe('buildSections', () => {
|
||||
'',
|
||||
'workspaceStatus',
|
||||
new Set(),
|
||||
new Map(),
|
||||
NO_PROJECT_SECTION_CATALOG,
|
||||
DEFAULT_MOBILE_WORKSPACE_STATUSES
|
||||
)
|
||||
|
||||
@@ -339,7 +348,7 @@ describe('buildSections', () => {
|
||||
'',
|
||||
'workspaceStatus',
|
||||
new Set(),
|
||||
new Map(),
|
||||
NO_PROJECT_SECTION_CATALOG,
|
||||
DEFAULT_MOBILE_WORKSPACE_STATUSES
|
||||
)
|
||||
|
||||
@@ -525,7 +534,7 @@ describe('buildSections', () => {
|
||||
'',
|
||||
'repo',
|
||||
new Set(),
|
||||
new Map([['orca', 'repo-1']]),
|
||||
catalogOf([['orca', 'repo-1']]),
|
||||
DEFAULT_MOBILE_WORKSPACE_STATUSES
|
||||
)
|
||||
|
||||
@@ -556,7 +565,7 @@ describe('buildSections', () => {
|
||||
'',
|
||||
'repo',
|
||||
new Set(),
|
||||
new Map([['orca', 'repo-1']])
|
||||
catalogOf([['orca', 'repo-1']])
|
||||
)
|
||||
|
||||
expect(sections[0]?.data.map((item) => item.worktreeId)).toEqual(['main', 'child'])
|
||||
@@ -570,7 +579,7 @@ describe('buildSections', () => {
|
||||
'',
|
||||
'repo',
|
||||
new Set(),
|
||||
new Map([
|
||||
catalogOf([
|
||||
['orca', 'repo-1'],
|
||||
['zoom-img', 'repo-missing']
|
||||
])
|
||||
@@ -594,7 +603,7 @@ describe('buildSections', () => {
|
||||
'',
|
||||
'none',
|
||||
new Set(),
|
||||
new Map([['zoom-img', 'repo-missing']])
|
||||
catalogOf([['zoom-img', 'repo-missing']])
|
||||
)
|
||||
|
||||
expect(withoutSectionListKeys(sections)).toEqual([])
|
||||
@@ -612,7 +621,7 @@ describe('buildSections', () => {
|
||||
'zoom',
|
||||
'repo',
|
||||
new Set(),
|
||||
new Map([
|
||||
catalogOf([
|
||||
['zoom-img', 'repo-matching'],
|
||||
['repo', 'repo-hidden'],
|
||||
['zoom-hidden', 'repo-unfiltered']
|
||||
@@ -637,7 +646,7 @@ describe('buildSections', () => {
|
||||
'',
|
||||
'repo',
|
||||
new Set(),
|
||||
new Map([
|
||||
catalogOf([
|
||||
['sleeping-repo', 'repo-sleeping'],
|
||||
['empty-repo', 'repo-empty']
|
||||
])
|
||||
@@ -667,7 +676,7 @@ describe('buildSections', () => {
|
||||
'',
|
||||
'workspaceStatus',
|
||||
new Set(),
|
||||
new Map(),
|
||||
NO_PROJECT_SECTION_CATALOG,
|
||||
DEFAULT_MOBILE_WORKSPACE_STATUSES
|
||||
)
|
||||
|
||||
@@ -691,7 +700,7 @@ describe('buildSections', () => {
|
||||
'',
|
||||
'workspaceStatus',
|
||||
new Set(),
|
||||
new Map(),
|
||||
NO_PROJECT_SECTION_CATALOG,
|
||||
[]
|
||||
)
|
||||
|
||||
@@ -720,7 +729,7 @@ describe('buildSections', () => {
|
||||
'',
|
||||
groupMode,
|
||||
new Set(['local-pinned']),
|
||||
new Map(),
|
||||
NO_PROJECT_SECTION_CATALOG,
|
||||
DEFAULT_MOBILE_WORKSPACE_STATUSES,
|
||||
new Set(),
|
||||
showPinnedInGroups
|
||||
@@ -759,7 +768,7 @@ describe('buildSections', () => {
|
||||
'',
|
||||
'repo',
|
||||
new Set(),
|
||||
new Map([['orca', 'repo-1']])
|
||||
catalogOf([['orca', 'repo-1']])
|
||||
)
|
||||
|
||||
expect(sections.map((section) => section.key)).toEqual(['pinned'])
|
||||
@@ -786,7 +795,7 @@ describe('buildSections', () => {
|
||||
'',
|
||||
'none',
|
||||
new Set(),
|
||||
new Map(),
|
||||
NO_PROJECT_SECTION_CATALOG,
|
||||
DEFAULT_MOBILE_WORKSPACE_STATUSES
|
||||
)
|
||||
|
||||
@@ -817,7 +826,7 @@ describe('buildSections', () => {
|
||||
'',
|
||||
'workspaceStatus',
|
||||
new Set(),
|
||||
new Map(),
|
||||
NO_PROJECT_SECTION_CATALOG,
|
||||
DEFAULT_MOBILE_WORKSPACE_STATUSES
|
||||
)
|
||||
|
||||
@@ -845,7 +854,7 @@ describe('buildSections', () => {
|
||||
'',
|
||||
'workspaceStatus',
|
||||
new Set(),
|
||||
new Map(),
|
||||
NO_PROJECT_SECTION_CATALOG,
|
||||
DEFAULT_MOBILE_WORKSPACE_STATUSES,
|
||||
new Set([getMobileWorkspaceLineageGroupKey(parent)])
|
||||
)
|
||||
|
||||
@@ -10,12 +10,57 @@ import {
|
||||
getMobileWorkspaceLineageChildren
|
||||
} from './mobile-workspace-lineage'
|
||||
import { getPRGroupKey, PR_GROUP_LABELS, PR_GROUP_ORDER } from './workspace-pr-status-groups'
|
||||
import type { FilterState, Section, Worktree } from './workspace-list-types'
|
||||
import type { FilterState, Section, SectionRepo, Worktree } from './workspace-list-types'
|
||||
import type { MobileGroupMode, MobileSortMode } from './workspace-view-settings'
|
||||
import { sortWorktrees } from './workspace-list-ordering'
|
||||
import { getWorktreeRowIdentity } from './worktree-host-row-identity'
|
||||
import {
|
||||
buildProjectGroupingIndex,
|
||||
buildProjectSections,
|
||||
type ProjectSectionModel
|
||||
} from '../../../src/shared/project-section-grouping'
|
||||
import { withRepoSectionDisplayLabels } from '../../../src/shared/project-section-order'
|
||||
import type { ProjectOrderBy } from '../../../src/shared/ui-chrome-types'
|
||||
|
||||
export type { FilterState, Section, Worktree } from './workspace-list-types'
|
||||
export type { FilterState, Section, SectionRepo, Worktree } from './workspace-list-types'
|
||||
|
||||
/** What Project grouping reads beyond the rows: the desktop's all-host catalog and header order. */
|
||||
export type ProjectSectionCatalog = {
|
||||
/** Catalog repos of the hosts the list shows; each without rows gets an empty section. */
|
||||
repos: readonly SectionRepo[]
|
||||
/** Null until the desktop's catalog answers; rows then group by repo, as the desktop's would. */
|
||||
grouping: ProjectSectionModel | null
|
||||
/** Manual header rank; undefined keeps first-row order. */
|
||||
repoOrder: ReadonlyMap<string, number> | undefined
|
||||
projectOrderBy: ProjectOrderBy
|
||||
}
|
||||
|
||||
export const NO_PROJECT_SECTION_CATALOG: ProjectSectionCatalog = {
|
||||
repos: [],
|
||||
grouping: null,
|
||||
repoOrder: undefined,
|
||||
projectOrderBy: 'manual'
|
||||
}
|
||||
|
||||
/** Catalog repos by id, plus a stand-in from the row for a repo its host's catalog lacks. */
|
||||
export function buildSectionRepoMap(
|
||||
catalogRepos: readonly SectionRepo[],
|
||||
worktrees: readonly Worktree[]
|
||||
): Map<string, SectionRepo> {
|
||||
const repoMap = new Map(catalogRepos.map((repo) => [repo.id, repo]))
|
||||
for (const w of worktrees) {
|
||||
if (!repoMap.has(w.repoId)) {
|
||||
repoMap.set(w.repoId, {
|
||||
id: w.repoId,
|
||||
displayName: w.repo || 'Unknown',
|
||||
// Why: labels key on host + path, so a shared '' would let one stand-in rename another.
|
||||
path: w.repoId,
|
||||
executionHostId: w.hostId
|
||||
})
|
||||
}
|
||||
}
|
||||
return repoMap
|
||||
}
|
||||
export { CREATE_GRACE_MS, getWorktreeStatus, sortWorktrees } from './workspace-list-ordering'
|
||||
|
||||
function makeSection(
|
||||
@@ -23,13 +68,14 @@ function makeSection(
|
||||
title: string,
|
||||
data: Worktree[],
|
||||
collapsedGroups: ReadonlySet<string>,
|
||||
icon?: 'pin'
|
||||
decoration: { icon?: 'pin'; repo?: SectionRepo } = {}
|
||||
): Section {
|
||||
const rows = applyMobileWorkspaceLineage(data, collapsedGroups)
|
||||
return {
|
||||
key,
|
||||
title,
|
||||
...(icon ? { icon } : {}),
|
||||
...(decoration.icon ? { icon: decoration.icon } : {}),
|
||||
...(decoration.repo ? { repo: decoration.repo } : {}),
|
||||
data: rows.map((worktree) => ({
|
||||
...worktree,
|
||||
sectionListKey: `${key}:${getWorktreeRowIdentity(worktree)}`
|
||||
@@ -149,7 +195,7 @@ export function buildSections(
|
||||
search: string,
|
||||
groupMode: MobileGroupMode,
|
||||
pinnedIds: Set<string>,
|
||||
repoIdsByName: ReadonlyMap<string, string> = new Map(),
|
||||
projectCatalog: ProjectSectionCatalog = NO_PROJECT_SECTION_CATALOG,
|
||||
workspaceStatuses: readonly WorkspaceStatusDefinition[] = DEFAULT_MOBILE_WORKSPACE_STATUSES,
|
||||
collapsedGroups: ReadonlySet<string> = new Set(),
|
||||
showPinnedInGroups = false
|
||||
@@ -164,7 +210,7 @@ export function buildSections(
|
||||
|
||||
const sections: Section[] = []
|
||||
if (pinned.length > 0) {
|
||||
sections.push(makeSection('pinned', 'Pinned', pinned, collapsedGroups, 'pin'))
|
||||
sections.push(makeSection('pinned', 'Pinned', pinned, collapsedGroups, { icon: 'pin' }))
|
||||
}
|
||||
|
||||
if (groupMode === 'none') {
|
||||
@@ -172,35 +218,34 @@ export function buildSections(
|
||||
sections.push(makeSection('all', 'All', canonicalGroupWorktrees, collapsedGroups))
|
||||
}
|
||||
} else if (groupMode === 'repo') {
|
||||
const byRepo = new Map<string, Worktree[]>()
|
||||
for (const w of canonicalGroupWorktrees) {
|
||||
const key = w.repo || 'Unknown'
|
||||
const list = byRepo.get(key)
|
||||
if (list) {
|
||||
list.push(w)
|
||||
} else {
|
||||
byRepo.set(key, [w])
|
||||
}
|
||||
}
|
||||
const representedRepoIds = new Set(worktrees.map((w) => w.repoId))
|
||||
const query = search.trim().toLowerCase()
|
||||
for (const [displayName, id] of repoIdsByName) {
|
||||
if (representedRepoIds.has(id)) {
|
||||
continue
|
||||
}
|
||||
if (filters.filterRepoIds.size > 0 && !filters.filterRepoIds.has(id)) {
|
||||
continue
|
||||
}
|
||||
if (query && !displayName.toLowerCase().includes(query)) {
|
||||
continue
|
||||
}
|
||||
if (!byRepo.has(displayName)) {
|
||||
byRepo.set(displayName, [])
|
||||
}
|
||||
}
|
||||
for (const [repo, items] of byRepo) {
|
||||
const key = `repo:${repoIdsByName.get(repo) ?? repo}`
|
||||
sections.push(makeSection(key, repo, orderMainWorktreeFirst(items), collapsedGroups))
|
||||
const placeholderRepoIds = projectCatalog.repos
|
||||
.filter(
|
||||
(repo) =>
|
||||
!representedRepoIds.has(repo.id) &&
|
||||
(filters.filterRepoIds.size === 0 || filters.filterRepoIds.has(repo.id)) &&
|
||||
(!query || repo.displayName.toLowerCase().includes(query))
|
||||
)
|
||||
.map((repo) => repo.id)
|
||||
// Why: the desktop sidebar's own grouping, so keys, labels and order match it.
|
||||
const projectSections = withRepoSectionDisplayLabels(
|
||||
buildProjectSections({
|
||||
items: canonicalGroupWorktrees,
|
||||
repoIdOf: (w) => w.repoId,
|
||||
repoMap: buildSectionRepoMap(projectCatalog.repos, worktrees),
|
||||
projectIndex: buildProjectGroupingIndex(projectCatalog.grouping ?? undefined),
|
||||
placeholderRepoIds,
|
||||
repoOrder: projectCatalog.repoOrder,
|
||||
projectOrderBy: projectCatalog.projectOrderBy
|
||||
})
|
||||
)
|
||||
for (const [key, section] of projectSections) {
|
||||
sections.push(
|
||||
makeSection(key, section.label, orderMainWorktreeFirst(section.items), collapsedGroups, {
|
||||
repo: section.repo
|
||||
})
|
||||
)
|
||||
}
|
||||
} else if (groupMode === 'workspaceStatus') {
|
||||
const renderableWorkspaceStatuses = coerceMobileWorkspaceStatuses(workspaceStatuses)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ExecutionHostId } from '../../../src/shared/execution-host'
|
||||
import type { AgentWorkingMode } from '../../../src/shared/agent-status-types'
|
||||
import type { RuntimeWorktreeAgentRow } from '../../../src/shared/runtime-types'
|
||||
import type { ProjectSectionRepo } from '../../../src/shared/project-section-grouping'
|
||||
|
||||
export type Worktree = {
|
||||
sectionListKey?: string
|
||||
@@ -65,4 +66,15 @@ export type FilterState = {
|
||||
alwaysShowDefaultBranch?: boolean
|
||||
}
|
||||
|
||||
export type Section = { key: string; title: string; icon?: 'pin'; data: Worktree[] }
|
||||
/** A repo as Project sections read it: the desktop's catalog record, or one a row stands in for. */
|
||||
// repoIcon stays unread wire data until drawn (readMobileRepoIcon).
|
||||
export type SectionRepo = ProjectSectionRepo & { badgeColor?: string; repoIcon?: unknown }
|
||||
|
||||
export type Section = {
|
||||
key: string
|
||||
title: string
|
||||
icon?: 'pin'
|
||||
/** Under Project grouping, the repo whose colour and icon the header shows, as on the desktop. */
|
||||
repo?: SectionRepo
|
||||
data: Worktree[]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildSections, type Worktree } from './workspace-list-sections'
|
||||
import { NO_PROJECT_SECTION_CATALOG, buildSections, type Worktree } from './workspace-list-sections'
|
||||
import {
|
||||
applyDesktopViewSettings,
|
||||
type MobileViewState,
|
||||
@@ -47,11 +47,11 @@ const base: MobileViewState = {
|
||||
}
|
||||
|
||||
// Reconstruct the list the host screen renders: apply the synced desktop
|
||||
// settings, then build sections exactly as the screen does. `repoIdsByName` is
|
||||
// the display-name-keyed map the screen builds from repo.list.
|
||||
// settings, then build sections exactly as the screen does. `catalogRepos` are
|
||||
// [displayName, id] rows of the repo.list catalog the screen groups by.
|
||||
function visibleWorktreeIds(
|
||||
worktrees: Worktree[],
|
||||
repoIdsByName: Map<string, string>,
|
||||
catalogRepos: readonly (readonly [string, string])[],
|
||||
desktop: WorkspaceViewSettings = {}
|
||||
): string[] {
|
||||
const state = applyDesktopViewSettings(base, desktop)
|
||||
@@ -66,7 +66,10 @@ function visibleWorktreeIds(
|
||||
'',
|
||||
state.groupMode,
|
||||
new Set(),
|
||||
repoIdsByName,
|
||||
{
|
||||
...NO_PROJECT_SECTION_CATALOG,
|
||||
repos: catalogRepos.map(([displayName, id]) => ({ id, displayName, path: `/repos/${id}` }))
|
||||
},
|
||||
state.workspaceStatuses,
|
||||
new Set(state.collapsedGroups)
|
||||
)
|
||||
@@ -81,9 +84,9 @@ describe('every worktree stays visible regardless of repo.list contents', () =>
|
||||
worktree('orca-local', 'b'),
|
||||
worktree('orca-runtime', 'c')
|
||||
]
|
||||
const repoIdsByName = new Map([['orca-runtime', 'orca-runtime']])
|
||||
const catalogRepos = [['orca-runtime', 'orca-runtime']] as const
|
||||
|
||||
expect(visibleWorktreeIds(worktrees, repoIdsByName)).toEqual(['a', 'b', 'c'])
|
||||
expect(visibleWorktreeIds(worktrees, catalogRepos)).toEqual(['a', 'b', 'c'])
|
||||
})
|
||||
|
||||
it('shows every worktree when the same repo name maps to two host-specific ids', () => {
|
||||
@@ -94,29 +97,26 @@ describe('every worktree stays visible regardless of repo.list contents', () =>
|
||||
worktree('orca-local', 'local-2', 'orca'),
|
||||
worktree('orca-runtime', 'runtime-1', 'orca')
|
||||
]
|
||||
// Name-keyed map collapses "orca" to a single id, as the host screen builds it.
|
||||
const repoIdsByName = new Map([['orca', 'orca-runtime']])
|
||||
// The catalog names only one of the two "orca" repos.
|
||||
const catalogRepos = [['orca', 'orca-runtime']] as const
|
||||
|
||||
expect(visibleWorktreeIds(worktrees, repoIdsByName)).toEqual([
|
||||
'local-1',
|
||||
'local-2',
|
||||
'runtime-1'
|
||||
])
|
||||
expect(visibleWorktreeIds(worktrees, catalogRepos)).toEqual(['local-1', 'local-2', 'runtime-1'])
|
||||
})
|
||||
|
||||
it('shows every worktree even when the desktop carries a host scope', () => {
|
||||
// Host scope must never reach the mobile list either (it has no mobile UI).
|
||||
const worktrees = [worktree('local-repo', 'x'), worktree('runtime-repo', 'y')]
|
||||
const repoIdsByName = new Map([
|
||||
const catalogRepos = [
|
||||
['local-repo', 'local-repo'],
|
||||
['runtime-repo', 'runtime-repo']
|
||||
])
|
||||
] as const
|
||||
|
||||
expect(
|
||||
visibleWorktreeIds(worktrees, repoIdsByName, {
|
||||
workspaceHostScope: 'runtime:devbox',
|
||||
visibleWorkspaceHostIds: ['runtime:devbox']
|
||||
} as unknown as WorkspaceViewSettings)
|
||||
).toEqual(['x', 'y'])
|
||||
// A variable, not a literal: `workspaceHostScope` is not a member the phone reads.
|
||||
const desktopWithHostScope = {
|
||||
workspaceHostScope: 'runtime:devbox',
|
||||
visibleWorkspaceHostIds: ['runtime:devbox']
|
||||
}
|
||||
|
||||
expect(visibleWorktreeIds(worktrees, catalogRepos, desktopWithHostScope)).toEqual(['x', 'y'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -23,6 +23,8 @@ vi.mock('electron', () => ({
|
||||
|
||||
import { registerRuntimeHandlers } from './runtime'
|
||||
import { TERMINAL_FIT_RESTORE_DEADLINE_MS } from '../../shared/terminal-fit-restore-deadline'
|
||||
import { PROJECT_SECTION_PARITY_SERVED } from '../../shared/project-section-parity.test-fixture'
|
||||
import { readMobileProjectSections } from '../runtime/mobile-project-sections-snapshot'
|
||||
|
||||
function runtimeCallEvent() {
|
||||
const mainFrame = {}
|
||||
@@ -72,6 +74,46 @@ describe('registerRuntimeHandlers', () => {
|
||||
expect(result).toEqual({ graphStatus: 'ready' })
|
||||
})
|
||||
|
||||
it("serves the authoritative window's sidebar catalog to paired phones, and keeps it when omitted", () => {
|
||||
const runtime = { syncWindowGraph: vi.fn().mockReturnValue({ graphStatus: 'ready' }) }
|
||||
registerRuntimeHandlers(runtime as never)
|
||||
const handler = handleMock.mock.calls.find(
|
||||
([channel]) => channel === 'runtime:syncWindowGraph'
|
||||
)![1]
|
||||
const currentMainFrame = {}
|
||||
const event = { sender: { mainFrame: currentMainFrame }, senderFrame: currentMainFrame }
|
||||
fromWebContentsMock.mockReturnValue({ id: 17 })
|
||||
const graph = { tabs: [], leaves: [], rendererGeneration: 'renderer-1' }
|
||||
|
||||
handler(event, { ...graph, projectSectionCatalog: PROJECT_SECTION_PARITY_SERVED })
|
||||
handler(event, graph)
|
||||
|
||||
expect(readMobileProjectSections()).toEqual({ catalog: PROJECT_SECTION_PARITY_SERVED })
|
||||
})
|
||||
|
||||
it('does not serve a catalog from a graph the runtime refused', () => {
|
||||
const refused = { ...PROJECT_SECTION_PARITY_SERVED, projectOrderBy: 'recent' as const }
|
||||
const runtime = {
|
||||
syncWindowGraph: vi.fn(() => {
|
||||
throw new Error('Runtime graph publisher does not match the authoritative window')
|
||||
})
|
||||
}
|
||||
registerRuntimeHandlers(runtime as never)
|
||||
const handler = handleMock.mock.calls.find(
|
||||
([channel]) => channel === 'runtime:syncWindowGraph'
|
||||
)![1]
|
||||
const currentMainFrame = {}
|
||||
fromWebContentsMock.mockReturnValue({ id: 18 })
|
||||
|
||||
expect(() =>
|
||||
handler(
|
||||
{ sender: { mainFrame: currentMainFrame }, senderFrame: currentMainFrame },
|
||||
{ tabs: [], leaves: [], rendererGeneration: 'renderer-2', projectSectionCatalog: refused }
|
||||
)
|
||||
).toThrow()
|
||||
expect(readMobileProjectSections().catalog).not.toEqual(refused)
|
||||
})
|
||||
|
||||
it('rejects a graph publication queued by a superseded main frame', () => {
|
||||
const runtime = {
|
||||
syncWindowGraph: vi.fn(),
|
||||
|
||||
@@ -15,6 +15,7 @@ import { RpcDispatcher } from '../runtime/rpc/dispatcher'
|
||||
import { DESKTOP_RPC_CALLER } from '../runtime/rpc/rpc-caller-identity'
|
||||
import { ALL_RPC_METHODS } from '../runtime/rpc/methods'
|
||||
import { DesktopRuntimeSenderLifecycle } from './desktop-runtime-sender-lifecycle'
|
||||
import { publishMobileProjectSections } from '../runtime/mobile-project-sections-snapshot'
|
||||
|
||||
function boundTerminalFitRestore(pending: Promise<boolean>): Promise<boolean> {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
@@ -49,7 +50,12 @@ export function registerRuntimeHandlers(runtime: OrcaRuntimeService): void {
|
||||
if (typeof graph.rendererGeneration !== 'string' || graph.rendererGeneration.length === 0) {
|
||||
throw new Error('Runtime graph sync requires a renderer generation')
|
||||
}
|
||||
return runtime.syncWindowGraph(window.id, graph)
|
||||
const result = runtime.syncWindowGraph(window.id, graph)
|
||||
// Why after the sync: only the authoritative window's graph is accepted, so only its catalog is.
|
||||
if (graph.projectSectionCatalog) {
|
||||
publishMobileProjectSections(graph.projectSectionCatalog)
|
||||
}
|
||||
return result
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type {
|
||||
MobileProjectSectionCatalog,
|
||||
MobileProjectSectionsResult
|
||||
} from '../../shared/mobile-project-sections-contract'
|
||||
|
||||
// Why kept after the window closes: a stale sidebar catalog groups better than none.
|
||||
let latestCatalog: MobileProjectSectionCatalog | null = null
|
||||
|
||||
/** The authoritative desktop window's sidebar catalog, as its runtime graph last carried it. */
|
||||
export function publishMobileProjectSections(catalog: MobileProjectSectionCatalog): void {
|
||||
latestCatalog = catalog
|
||||
}
|
||||
|
||||
export function readMobileProjectSections(): MobileProjectSectionsResult {
|
||||
return { catalog: latestCatalog }
|
||||
}
|
||||
@@ -49,6 +49,7 @@ import { RUNTIME_CLIENT_CAPABILITY_METHODS } from './runtime-client-capabilities
|
||||
import { EMULATOR_METHODS } from './emulator'
|
||||
import { PAIRING_METHODS } from './pairing'
|
||||
import { MOBILE_RELAY_HOSTS_METHODS } from './mobile-relay-hosts'
|
||||
import { MOBILE_PROJECT_SECTIONS_METHODS } from './mobile-project-sections'
|
||||
import { UPDATER_METHODS } from './updater'
|
||||
import { AUTOMATIC_WORK_METHODS } from './automatic-work'
|
||||
import { AGENT_SESSION_METHODS } from './agent-session'
|
||||
@@ -125,6 +126,7 @@ export const ALL_RPC_METHODS = [
|
||||
...EMULATOR_METHODS,
|
||||
...PAIRING_METHODS,
|
||||
...MOBILE_RELAY_HOSTS_METHODS,
|
||||
...MOBILE_PROJECT_SECTIONS_METHODS,
|
||||
...UPDATER_METHODS,
|
||||
...AUTOMATIC_WORK_METHODS
|
||||
]
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import '../unused-default-rpc-methods.test-fixture'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { RpcDispatcher } from '../dispatcher'
|
||||
import type { OrcaRuntimeService } from '../../orca-runtime'
|
||||
import { MOBILE_PROJECT_SECTIONS_METHOD } from '../../../../shared/mobile-project-sections-contract'
|
||||
import { PROJECT_SECTION_PARITY_SERVED } from '../../../../shared/project-section-parity.test-fixture'
|
||||
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the method reads no runtime member; the dispatcher only stamps the runtime id.
|
||||
const runtime = { getRuntimeId: () => 'test-runtime' } as unknown as OrcaRuntimeService
|
||||
|
||||
// Why fresh modules: the snapshot is process state, so each test starts with nothing published.
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
async function sectionsDispatcher(): Promise<RpcDispatcher> {
|
||||
const { MOBILE_PROJECT_SECTIONS_METHODS } = await import('./mobile-project-sections')
|
||||
return new RpcDispatcher({ runtime, methods: MOBILE_PROJECT_SECTIONS_METHODS })
|
||||
}
|
||||
|
||||
describe(MOBILE_PROJECT_SECTIONS_METHOD, () => {
|
||||
it('answers no catalog before a desktop window has published one', async () => {
|
||||
const response = await (
|
||||
await sectionsDispatcher()
|
||||
).dispatch({
|
||||
id: 'req-1',
|
||||
authToken: 'tok',
|
||||
method: MOBILE_PROJECT_SECTIONS_METHOD
|
||||
})
|
||||
|
||||
expect(response).toMatchObject({ ok: true, result: { catalog: null } })
|
||||
})
|
||||
|
||||
it("answers the window's last published catalog", async () => {
|
||||
const dispatcher = await sectionsDispatcher()
|
||||
const { publishMobileProjectSections } = await import('../../mobile-project-sections-snapshot')
|
||||
publishMobileProjectSections(PROJECT_SECTION_PARITY_SERVED)
|
||||
|
||||
const response = await dispatcher.dispatch({
|
||||
id: 'req-2',
|
||||
authToken: 'tok',
|
||||
method: MOBILE_PROJECT_SECTIONS_METHOD
|
||||
})
|
||||
|
||||
expect(response).toMatchObject({ ok: true, result: { catalog: PROJECT_SECTION_PARITY_SERVED } })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
import { defineMethod } from '../core'
|
||||
import { MOBILE_PROJECT_SECTIONS_METHOD } from '../../../../shared/mobile-project-sections-contract'
|
||||
import { readMobileProjectSections } from '../../mobile-project-sections-snapshot'
|
||||
|
||||
export const MOBILE_PROJECT_SECTIONS_METHODS = [
|
||||
defineMethod({
|
||||
name: MOBILE_PROJECT_SECTIONS_METHOD,
|
||||
permission: 'workspace',
|
||||
params: null,
|
||||
handler: () => readMobileProjectSections()
|
||||
})
|
||||
]
|
||||
@@ -16,6 +16,8 @@ export const PAIRED_DESKTOP_METHODS = [
|
||||
// The configured servers the desktop shows, and their workspace lists as it last fetched them.
|
||||
'mobileRelay.hosts.list',
|
||||
'mobileRelay.hosts.worktrees',
|
||||
// The sidebar's Project grouping inputs, as the desktop window last published them.
|
||||
'mobileRelay.projectSections',
|
||||
'speech.dictation.cancel',
|
||||
'speech.dictation.chunk',
|
||||
'speech.dictation.finish',
|
||||
|
||||
@@ -51,6 +51,22 @@ export function useRuntimeGraphSync(): void {
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
useAppStore.subscribe((state, previousState) => {
|
||||
// Why: the graph carries the sidebar's Project catalog to paired phones; republish on change.
|
||||
if (
|
||||
state.repos !== previousState.repos ||
|
||||
state.projects !== previousState.projects ||
|
||||
state.projectHostSetups !== previousState.projectHostSetups ||
|
||||
state.projectOrderBy !== previousState.projectOrderBy
|
||||
) {
|
||||
scheduleRuntimeGraphSync()
|
||||
}
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setRuntimeGraphSyncEnabled(workspaceSessionReady)
|
||||
return () => {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { getRepoDisplayLabelKey, getRepoDisplayLabelsByPath } from '@/lib/repo-display-labels'
|
||||
import {
|
||||
getRepoDisplayLabelKey,
|
||||
getRepoDisplayLabelsByPath
|
||||
} from '../../../../shared/repo-display-labels'
|
||||
import {
|
||||
getRepoExecutionHostId,
|
||||
getWorktreeExecutionHostId,
|
||||
@@ -8,8 +11,8 @@ import {
|
||||
import type { Project, ProjectHostSetup } from '../../../../shared/project-types'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import type { Worktree } from '../../../../shared/worktree/types'
|
||||
import { getProjectHeaderRevealTarget } from '../sidebar/worktree-list/grouping/project-grouping'
|
||||
import type { ProjectGroupingModel } from '../sidebar/worktree-list/grouping/project-grouping'
|
||||
import { getProjectHeaderRevealTarget } from '../../../../shared/project-section-grouping'
|
||||
import type { ProjectGroupingModel } from '../../../../shared/project-section-grouping'
|
||||
import type { SidebarHostOption } from '../sidebar/sidebar-host-options'
|
||||
import {
|
||||
expandEquivalentExecutionHostIds,
|
||||
|
||||
@@ -12,8 +12,8 @@ import type { Project, ProjectHostSetup } from '../../../../shared/project-types
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { getProjectGroupHeaderKey } from '../sidebar/worktree-list/grouping/group-keys'
|
||||
import { getProjectHeaderRevealTarget } from '../sidebar/worktree-list/grouping/project-grouping'
|
||||
import type { ProjectGroupingModel } from '../sidebar/worktree-list/grouping/project-grouping'
|
||||
import { getProjectHeaderRevealTarget } from '../../../../shared/project-section-grouping'
|
||||
import type { ProjectGroupingModel } from '../../../../shared/project-section-grouping'
|
||||
|
||||
export type CmdJProjectGroupResult = {
|
||||
id: string
|
||||
|
||||
@@ -2,7 +2,10 @@ import { useCallback, useMemo, type Dispatch, type SetStateAction } from 'react'
|
||||
import { GitBranch } from 'lucide-react'
|
||||
import type { NestedRepoScanResult } from '../../../../shared/project-group-types'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { getRepoDisplayLabelKey, getRepoDisplayLabelsByPath } from '@/lib/repo-display-labels'
|
||||
import {
|
||||
getRepoDisplayLabelKey,
|
||||
getRepoDisplayLabelsByPath
|
||||
} from '../../../../shared/repo-display-labels'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
function NestedRepoSelectAllRow({
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
buildProjectGroupingIndex,
|
||||
isCheckoutScopedProjectSetup,
|
||||
type ProjectGroupingModel
|
||||
} from '@/components/sidebar/worktree-list/grouping/project-grouping'
|
||||
} from '../../../../shared/project-section-grouping'
|
||||
|
||||
export type SettingsProject = {
|
||||
projectId: string
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createUIStore } from '../../store/slices/ui-slice-test-harness'
|
||||
import {
|
||||
buildProjectGroupingIndex,
|
||||
getProjectGroupingForRepo
|
||||
} from '../sidebar/worktree-list/grouping/project-grouping'
|
||||
} from '../../../../shared/project-section-grouping'
|
||||
import {
|
||||
buildSettingsProjectList,
|
||||
getSettingsEntryHostSelection,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getLogicalRepoOrderRankById } from '../../../../shared/project-section-order'
|
||||
import {
|
||||
applyAllRepoInsertAt,
|
||||
getLogicalRepoOrderRankById,
|
||||
getProjectGroupOrderForSidebarDrop,
|
||||
mapSidebarProjectHeaderDropIndexToSiblingInsertIndex,
|
||||
mapSidebarRepoDropIndexToAllRepoInsertAt
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getLogicalRepoOrderRankById } from '../../../../shared/project-section-order'
|
||||
|
||||
import {
|
||||
applyAllRepoInsertAt,
|
||||
computeProjectHeaderDropPreview,
|
||||
getLogicalRepoOrderRankById,
|
||||
getProjectGroupOrderForSidebarDrop,
|
||||
getProjectHeaderDragBucketKey,
|
||||
getSidebarOrderedRepoHeaderIdsByBucket,
|
||||
|
||||
@@ -44,20 +44,6 @@ export function getSidebarOrderedRepoHeaderIdsByBucket(
|
||||
return buckets
|
||||
}
|
||||
|
||||
export function getLogicalRepoOrderRankById(
|
||||
orderedRepoIds: readonly string[]
|
||||
): Map<string, number> {
|
||||
const rankById = new Map<string, number>()
|
||||
orderedRepoIds.forEach((repoId, index) => {
|
||||
// Why: paired hosts can contribute the same logical project ID; its merged
|
||||
// header must anchor to the first occurrence instead of whichever host loaded last.
|
||||
if (!rankById.has(repoId)) {
|
||||
rankById.set(repoId, index)
|
||||
}
|
||||
})
|
||||
return rankById
|
||||
}
|
||||
|
||||
export function getProjectGroupOrderForSidebarDrop(args: {
|
||||
siblings: readonly Repo[]
|
||||
dropIndex: number
|
||||
|
||||
@@ -12,7 +12,7 @@ import { getPinnedWorktreeDisplayPolicy } from './worktree-list/grouping/row-typ
|
||||
import { addHostSectionRows, type HostSectionRow } from './host-section-rows'
|
||||
import { orderHostSectionOptions } from './host-section-order'
|
||||
import { buildSidebarHostOptions } from './sidebar-host-options'
|
||||
import { getLogicalRepoOrderRankById } from './project-header-drop'
|
||||
import { getLogicalRepoOrderRankById } from '../../../../shared/project-section-order'
|
||||
import { getRenderedWorktreesInSidebarOrder } from './worktree-sidebar-row-preference'
|
||||
import { selectWorktreeListReviewCacheInputs } from './worktree-list/listing/review-cache-inputs'
|
||||
import {
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { applyManualRepoOrder } from '../../../../shared/manual-repo-order'
|
||||
import { mergeFetchedHostProjectCatalog } from '../../../../shared/project-compatibility-host-merge'
|
||||
import type { ProjectCatalogRows } from '../../../../shared/project-compatibility-merge'
|
||||
import {
|
||||
buildProjectGroupingIndex,
|
||||
buildProjectSections
|
||||
} from '../../../../shared/project-section-grouping'
|
||||
import {
|
||||
getLogicalRepoOrderRankById,
|
||||
withRepoSectionDisplayLabels
|
||||
} from '../../../../shared/project-section-order'
|
||||
import {
|
||||
PROJECT_SECTION_PARITY_HEADERS,
|
||||
PROJECT_SECTION_PARITY_HOSTS,
|
||||
PROJECT_SECTION_PARITY_ROWS,
|
||||
PROJECT_SECTION_PARITY_SERVED
|
||||
} from '../../../../shared/project-section-parity.test-fixture'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target'
|
||||
import type { ProjectOrderBy } from '../../../../shared/ui-chrome-types'
|
||||
import type { Worktree } from '../../../../shared/worktree/types'
|
||||
import {
|
||||
fetchRepoCatalogForTarget,
|
||||
filterSetupsForPrunedRepoRows,
|
||||
mergeFetchedRepoCatalog,
|
||||
reconcileSupersededSshRepos
|
||||
} from '../../store/repos/repo-catalog-merge'
|
||||
import { getProjectHostSetupProjectionFromState } from '../../store/project-host-setup-selector'
|
||||
import { buildRows } from './worktree-list/grouping/build-rows'
|
||||
import { worktree as worktreeFixture } from './worktree-list-groups-test-fixtures'
|
||||
import type { MobileProjectSectionCatalog } from '../../../../shared/mobile-project-sections-contract'
|
||||
import { getProjectSectionCatalogFromState } from '../../runtime/sync-runtime-graph/project-section-catalog'
|
||||
|
||||
// Why: the phone's Project sections must be the desktop sidebar's by construction. The desktop
|
||||
// half answers the renderer's own catalog fetch and runs its merge, selector, repo order and
|
||||
// buildRows; the phone half groups what main serves from the renderer's published catalog, and
|
||||
// mobile/src/worktree/workspace-list-sections-desktop-parity.test.ts runs buildSections on it.
|
||||
|
||||
const { rpc, assertCapability } = vi.hoisted(() => ({ rpc: vi.fn(), assertCapability: vi.fn() }))
|
||||
vi.mock('../../runtime/runtime-rpc-client', () => ({
|
||||
callRuntimeRpc: rpc,
|
||||
assertRuntimeEnvironmentCapability: assertCapability
|
||||
}))
|
||||
|
||||
function hostFor(target: RuntimeClientTarget) {
|
||||
const host = PROJECT_SECTION_PARITY_HOSTS.find((entry) =>
|
||||
target.kind === 'local'
|
||||
? entry.target.kind === 'local'
|
||||
: entry.target.kind === 'environment' && entry.target.environmentId === target.environmentId
|
||||
)
|
||||
if (!host) {
|
||||
throw new Error('unknown parity host')
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
function answerHosts(): void {
|
||||
const local = hostFor({ kind: 'local' })
|
||||
vi.stubGlobal('window', {
|
||||
api: {
|
||||
repos: { list: async () => local.repos },
|
||||
projects: {
|
||||
list: async () => local.lists?.projects ?? [],
|
||||
listHostSetups: async () => local.lists?.setups ?? []
|
||||
}
|
||||
}
|
||||
})
|
||||
rpc.mockImplementation(async (target: RuntimeClientTarget, method: string) => {
|
||||
const host = hostFor(target)
|
||||
if (method === 'repo.list') {
|
||||
return { repos: host.repos }
|
||||
}
|
||||
if (!host.lists) {
|
||||
throw new Error('method_not_found')
|
||||
}
|
||||
return method === 'project.list'
|
||||
? { projects: host.lists.projects }
|
||||
: { setups: host.lists.setups }
|
||||
})
|
||||
}
|
||||
|
||||
async function desktopCatalog(): Promise<{ repos: readonly Repo[]; rows: ProjectCatalogRows }> {
|
||||
answerHosts()
|
||||
// The all-host load's applyCatalog, host by host: local first, then each server.
|
||||
let repos: readonly Repo[] = []
|
||||
let rows: ProjectCatalogRows = { projects: [], projectHostSetups: [] }
|
||||
for (const host of PROJECT_SECTION_PARITY_HOSTS) {
|
||||
const catalog = await fetchRepoCatalogForTarget(host.target)
|
||||
const result = mergeFetchedRepoCatalog(catalog, repos)
|
||||
const reconciled = reconcileSupersededSshRepos(result.repos, { pendingSshRepoReadoptions: [] })
|
||||
const finalized = applyManualRepoOrder(reconciled.repos, [])
|
||||
rows = mergeFetchedHostProjectCatalog({
|
||||
previous: {
|
||||
projects: rows.projects,
|
||||
projectHostSetups: filterSetupsForPrunedRepoRows(
|
||||
rows.projectHostSetups,
|
||||
result.repos,
|
||||
finalized
|
||||
)
|
||||
},
|
||||
fetched: catalog.projectHostSetupCompatibility,
|
||||
repos: finalized,
|
||||
hostId: result.hostId
|
||||
})
|
||||
repos = finalized
|
||||
}
|
||||
return { repos, rows }
|
||||
}
|
||||
|
||||
async function desktopHeaders(projectOrderBy: ProjectOrderBy) {
|
||||
const { repos, rows } = await desktopCatalog()
|
||||
const projection = getProjectHostSetupProjectionFromState({ repos, ...rows })
|
||||
const worktrees: Worktree[] = PROJECT_SECTION_PARITY_ROWS.map((row) => ({
|
||||
...worktreeFixture,
|
||||
id: row.id,
|
||||
repoId: row.repoId,
|
||||
lastActivityAt: row.lastActivityAt
|
||||
}))
|
||||
const withWorktrees = new Set(PROJECT_SECTION_PARITY_ROWS.map((row) => row.repoId))
|
||||
return buildRows(
|
||||
'repo',
|
||||
worktrees,
|
||||
new Map(repos.map((repo) => [repo.id, repo])),
|
||||
null,
|
||||
new Set(),
|
||||
// As use-section-rows ranks it: the order of state.repos.
|
||||
getLogicalRepoOrderRankById(repos.map((repo) => repo.id)),
|
||||
undefined,
|
||||
projectOrderBy,
|
||||
{},
|
||||
undefined,
|
||||
false,
|
||||
undefined,
|
||||
[],
|
||||
new Set(repos.filter((repo) => !withWorktrees.has(repo.id)).map((repo) => repo.id)),
|
||||
new Map(),
|
||||
new Map(),
|
||||
[],
|
||||
{ projects: projection.projects, projectHostSetups: projection.setups }
|
||||
).flatMap((row) => (row.type === 'header' ? [{ key: row.key, label: row.label }] : []))
|
||||
}
|
||||
|
||||
/**
|
||||
* What main serves the phone: the renderer's published catalog as JSON crosses the wire. Main
|
||||
* stores and answers it unchanged (src/main/ipc/runtime.test.ts, mobile-project-sections.test.ts).
|
||||
*/
|
||||
async function servedCatalog(projectOrderBy: ProjectOrderBy): Promise<MobileProjectSectionCatalog> {
|
||||
const { repos, rows } = await desktopCatalog()
|
||||
const published = getProjectSectionCatalogFromState({ repos, ...rows, projectOrderBy })
|
||||
if (!published) {
|
||||
throw new Error('the renderer published no catalog')
|
||||
}
|
||||
return JSON.parse(JSON.stringify(published))
|
||||
}
|
||||
|
||||
/** The phone's grouping over a served catalog (mobile/src/worktree/project-section-catalog.ts). */
|
||||
function phoneHeaders(served: MobileProjectSectionCatalog) {
|
||||
const withRows = new Set(PROJECT_SECTION_PARITY_ROWS.map((row) => row.repoId))
|
||||
return withRepoSectionDisplayLabels(
|
||||
buildProjectSections({
|
||||
items: PROJECT_SECTION_PARITY_ROWS,
|
||||
repoIdOf: (row) => row.repoId,
|
||||
repoMap: new Map(served.repos.map((repo) => [repo.id, repo])),
|
||||
projectIndex: buildProjectGroupingIndex({
|
||||
projects: served.projects,
|
||||
projectHostSetups: served.setups
|
||||
}),
|
||||
placeholderRepoIds: served.repos
|
||||
.filter((repo) => !withRows.has(repo.id))
|
||||
.map((repo) => repo.id),
|
||||
repoOrder: getLogicalRepoOrderRankById(served.repos.map((repo) => repo.id)),
|
||||
projectOrderBy: served.projectOrderBy
|
||||
})
|
||||
).map(([key, section]) => ({ key, label: section.label }))
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
describe('the phone groups by Project exactly as the paired desktop sidebar', () => {
|
||||
it.each(['recent', 'manual'] as const)(
|
||||
'gives checkouts of one project on local and servers the same %s headers',
|
||||
async (projectOrderBy) => {
|
||||
expect(await desktopHeaders(projectOrderBy)).toEqual(
|
||||
PROJECT_SECTION_PARITY_HEADERS[projectOrderBy]
|
||||
)
|
||||
const served = await servedCatalog(projectOrderBy)
|
||||
expect(served).toEqual({ ...PROJECT_SECTION_PARITY_SERVED, projectOrderBy })
|
||||
expect(phoneHeaders(served)).toEqual(PROJECT_SECTION_PARITY_HEADERS[projectOrderBy])
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -12,7 +12,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildRows } from './worktree-list/grouping/build-rows'
|
||||
import { getNoticeHostContextLabels } from './worktree-list/grouping/host-labels'
|
||||
import { buildProjectGroupingIndex } from './worktree-list/grouping/project-grouping'
|
||||
import { buildProjectGroupingIndex } from '../../../../shared/project-section-grouping'
|
||||
import { repo, worktree, project, projectHostSetups } from './worktree-list-groups-test-fixtures'
|
||||
import type { ExecutionHostId } from '../../../../shared/execution-host'
|
||||
import type { ProjectHostSetup } from '../../../../shared/project-types'
|
||||
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
getMixedWorktreeHostContextLabels,
|
||||
getNoticeHostContextLabels
|
||||
} from './host-labels'
|
||||
import { buildProjectGroupingIndex } from './project-grouping'
|
||||
import type { ProjectGroupingModel } from './project-grouping'
|
||||
import { buildProjectGroupingIndex } from '../../../../../../shared/project-section-grouping'
|
||||
import type { ProjectGroupingModel } from '../../../../../../shared/project-section-grouping'
|
||||
import { appendProjectGroupSections } from './project-group-sections'
|
||||
import { getPinnedSectionWorktrees } from '../../pinned-section-worktrees'
|
||||
import { emitPinnedGroup } from './pinned-group-rows'
|
||||
@@ -42,7 +42,8 @@ import type {
|
||||
Row,
|
||||
WorktreeGroupBy
|
||||
} from './row-types'
|
||||
import { getRenderedNaturalAnchorRepoIds, withRepoSectionDisplayLabels } from './section-order'
|
||||
import { getRenderedNaturalAnchorRepoIds } from './section-order'
|
||||
import { withRepoSectionDisplayLabels } from '../../../../../../shared/project-section-order'
|
||||
import { buildOrderedGroups } from './worktree-grouping'
|
||||
|
||||
export function buildRows(
|
||||
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
getLaneHostWorktreeIds,
|
||||
getMixedHostContextLabels
|
||||
} from './host-labels'
|
||||
import type { OrderedGroupEntry, ProjectGroupingIndex } from './project-grouping'
|
||||
import type { OrderedGroupEntry } from './worktree-group-entry'
|
||||
import type { ProjectGroupingIndex } from '../../../../../../shared/project-section-grouping'
|
||||
import {
|
||||
appendWorktreeRows,
|
||||
buildFolderWorkspaceRow,
|
||||
|
||||
@@ -12,9 +12,9 @@ import {
|
||||
import { getWorktreeHostIdentity } from '../../../../../../shared/worktree/host-qualified-identity'
|
||||
import {
|
||||
getProjectGroupingForRepo,
|
||||
type ProjectGroupingIndex,
|
||||
type WorktreeGroupEntry
|
||||
} from './project-grouping'
|
||||
type ProjectGroupingIndex
|
||||
} from '../../../../../../shared/project-section-grouping'
|
||||
import type { WorktreeGroupEntry } from './worktree-group-entry'
|
||||
import { getFolderWorkspaceHostId } from '../../folder-workspace-host-id'
|
||||
import type { RenderableFolderWorkspace } from './folder-workspace-lanes'
|
||||
|
||||
|
||||
+2
-2
@@ -8,12 +8,12 @@ import { getEffectiveProjectGroupManualRank } from '../../../../../../shared/pro
|
||||
import { PROJECT_GROUP_META, getProjectGroupHeaderKey } from './group-keys'
|
||||
import { appendOrderedGroups } from './group-sections'
|
||||
import type { SectionAppendContext } from './group-sections'
|
||||
import type { OrderedGroupEntry } from './project-grouping'
|
||||
import type { OrderedGroupEntry } from './worktree-group-entry'
|
||||
import {
|
||||
compareRecentRank,
|
||||
createRecentRankLookup,
|
||||
withRepoSectionDisplayLabels
|
||||
} from './section-order'
|
||||
} from '../../../../../../shared/project-section-order'
|
||||
import { buildFolderWorkspaceRow } from './row-builders'
|
||||
|
||||
export function appendProjectGroupSections(
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
import type { Project, ProjectHostSetup } from '../../../../../../shared/project-types'
|
||||
import type { Repo } from '../../../../../../shared/repo-types'
|
||||
import type { Worktree } from '../../../../../../shared/worktree/types'
|
||||
import type { RenderableFolderWorkspace } from './folder-workspace-lanes'
|
||||
import { toSshExecutionHostId } from '../../../../../../shared/execution-host'
|
||||
import { parseWslUncPath } from '../../../../../../shared/wsl-paths'
|
||||
import {
|
||||
isWindowsAbsolutePathLike,
|
||||
normalizeRuntimePathForComparison
|
||||
} from '../../../../../../shared/cross-platform-path'
|
||||
|
||||
export type OrderedGroupEntry = [string, WorktreeGroupEntry]
|
||||
|
||||
export type ProjectGroupingModel = {
|
||||
projects: readonly Project[]
|
||||
projectHostSetups: readonly ProjectHostSetup[]
|
||||
}
|
||||
|
||||
export type WorktreeGroupEntry = {
|
||||
label: string
|
||||
items: Worktree[]
|
||||
repo?: Repo
|
||||
repoIds: Set<string>
|
||||
/** Folder workspaces bucketed into this lane under non-repo grouping. Carries
|
||||
* the owning group because FolderWorkspaceRow requires a non-optional one. */
|
||||
folderWorkspaces?: RenderableFolderWorkspace[]
|
||||
}
|
||||
|
||||
export type ProjectGroupingIndex = {
|
||||
projectById: Map<string, Project>
|
||||
setupByRepoId: Map<string, ProjectHostSetup>
|
||||
surfaceKeysRequiringSetupGroups: Set<string>
|
||||
}
|
||||
|
||||
const projectGroupingIndexCache = new WeakMap<ProjectGroupingModel, ProjectGroupingIndex | null>()
|
||||
|
||||
// Why: provisioned and folder setups are not independent Git checkouts.
|
||||
function isDistinctUserCheckout(setup: ProjectHostSetup): boolean {
|
||||
return setup.setupMethod !== 'provisioned' && setup.kind !== 'folder'
|
||||
}
|
||||
|
||||
// Why: execution target and filesystem namespace independently identify a surface.
|
||||
function getProjectSetupSurfaceKey(setup: ProjectHostSetup): string {
|
||||
return `${setup.projectId}::${setup.hostId}::${getExecutionSurface(setup)}::${getPathSurface(setup)}`
|
||||
}
|
||||
|
||||
function getExecutionSurface(setup: ProjectHostSetup): string {
|
||||
const connectionId = setup.connectionId?.trim()
|
||||
if (connectionId) {
|
||||
return toSshExecutionHostId(connectionId)
|
||||
}
|
||||
return setup.executionHostId?.trim() || setup.hostId
|
||||
}
|
||||
|
||||
// Why: projection twins differ by row identity, not checkout directory.
|
||||
function getCheckoutIdentity(setup: ProjectHostSetup): string {
|
||||
return normalizeRuntimePathForComparison(setup.path.trim()) || setup.repoId || setup.id
|
||||
}
|
||||
|
||||
function getPathSurface(setup: ProjectHostSetup): string {
|
||||
const wslPath = parseWslUncPath(setup.path)
|
||||
if (wslPath) {
|
||||
return `wsl:${wslPath.distro.toLowerCase()}`
|
||||
}
|
||||
if (isWindowsAbsolutePathLike(setup.path)) {
|
||||
return 'windows-host'
|
||||
}
|
||||
return 'default'
|
||||
}
|
||||
|
||||
export function buildProjectGroupingIndex(
|
||||
model?: ProjectGroupingModel
|
||||
): ProjectGroupingIndex | null {
|
||||
if (!model) {
|
||||
return null
|
||||
}
|
||||
const cached = projectGroupingIndexCache.get(model)
|
||||
if (cached !== undefined) {
|
||||
return cached
|
||||
}
|
||||
const projects = model.projects ?? []
|
||||
const projectHostSetups = model.projectHostSetups ?? []
|
||||
if (projects.length === 0 || projectHostSetups.length === 0) {
|
||||
projectGroupingIndexCache.set(model, null)
|
||||
return null
|
||||
}
|
||||
const checkoutsByProjectSurface = new Map<string, Set<string>>()
|
||||
for (const setup of projectHostSetups) {
|
||||
if (!isDistinctUserCheckout(setup)) {
|
||||
continue
|
||||
}
|
||||
const key = getProjectSetupSurfaceKey(setup)
|
||||
const existing = checkoutsByProjectSurface.get(key)
|
||||
if (existing) {
|
||||
existing.add(getCheckoutIdentity(setup))
|
||||
} else {
|
||||
checkoutsByProjectSurface.set(key, new Set([getCheckoutIdentity(setup)]))
|
||||
}
|
||||
}
|
||||
const surfaceKeysRequiringSetupGroups = new Set<string>()
|
||||
for (const [surfaceKey, checkouts] of checkoutsByProjectSurface) {
|
||||
if (checkouts.size > 1) {
|
||||
surfaceKeysRequiringSetupGroups.add(surfaceKey)
|
||||
}
|
||||
}
|
||||
const index = {
|
||||
projectById: new Map(projects.map((project) => [project.id, project])),
|
||||
setupByRepoId: new Map(projectHostSetups.map((setup) => [setup.repoId, setup])),
|
||||
surfaceKeysRequiringSetupGroups
|
||||
}
|
||||
projectGroupingIndexCache.set(model, index)
|
||||
return index
|
||||
}
|
||||
|
||||
// Why: Settings splits projects by this same rule so it matches the sidebar headers.
|
||||
export function isCheckoutScopedProjectSetup(
|
||||
setup: ProjectHostSetup,
|
||||
projectIndex: ProjectGroupingIndex
|
||||
): boolean {
|
||||
return (
|
||||
projectIndex.surfaceKeysRequiringSetupGroups.has(getProjectSetupSurfaceKey(setup)) &&
|
||||
isDistinctUserCheckout(setup)
|
||||
)
|
||||
}
|
||||
|
||||
export type ProjectHeaderRevealTarget = {
|
||||
key: string
|
||||
label: string
|
||||
repo?: Repo
|
||||
projectId?: string
|
||||
}
|
||||
|
||||
export function getProjectGroupingForRepo(
|
||||
repoId: string,
|
||||
repoMap: Map<string, Repo>,
|
||||
projectIndex: ProjectGroupingIndex | null
|
||||
): ProjectHeaderRevealTarget {
|
||||
const repo = repoMap.get(repoId)
|
||||
const setup = projectIndex?.setupByRepoId.get(repoId)
|
||||
const project = setup ? projectIndex?.projectById.get(setup.projectId) : undefined
|
||||
if (!setup || !project) {
|
||||
return {
|
||||
key: `repo:${repoId}`,
|
||||
label: repo?.displayName ?? 'Unknown',
|
||||
repo
|
||||
}
|
||||
}
|
||||
if (projectIndex && isCheckoutScopedProjectSetup(setup, projectIndex)) {
|
||||
// Why: only the ambiguous surface needs checkout-specific headers.
|
||||
return {
|
||||
key: `project:${project.id}::setup:${repoId}`,
|
||||
label: repo?.displayName ?? setup.displayName,
|
||||
repo,
|
||||
projectId: project.id
|
||||
}
|
||||
}
|
||||
// Why: provisioned runtime copies and non-ambiguous checkouts follow project
|
||||
// identity rather than path-scoped setup identity, so they stay in one project.
|
||||
return {
|
||||
key: `project:${project.id}`,
|
||||
label: project.displayName,
|
||||
repo,
|
||||
projectId: project.id
|
||||
}
|
||||
}
|
||||
|
||||
export function getProjectHeaderRevealTarget(
|
||||
repoId: string,
|
||||
repoMap: Map<string, Repo>,
|
||||
projectGrouping?: ProjectGroupingModel
|
||||
): ProjectHeaderRevealTarget {
|
||||
return getProjectGroupingForRepo(repoId, repoMap, buildProjectGroupingIndex(projectGrouping))
|
||||
}
|
||||
|
||||
export function addRepoIdToGroup(group: WorktreeGroupEntry, repoId: string): void {
|
||||
group.repoIds.add(repoId)
|
||||
}
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { sortProjectEntries } from './section-order'
|
||||
import { sortProjectEntries } from '../../../../../../shared/project-section-order'
|
||||
import { appendProjectGroupSections } from './project-group-sections'
|
||||
import type { SectionAppendContext } from './group-sections'
|
||||
import { PROJECT_GROUP_META } from './group-keys'
|
||||
import type { OrderedGroupEntry } from './project-grouping'
|
||||
import type { OrderedGroupEntry } from './worktree-group-entry'
|
||||
import { repo, worktree } from '../../worktree-list-groups-test-fixtures'
|
||||
|
||||
function entry(
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import type { Repo } from '../../../../../../shared/repo-types'
|
||||
import type { ProjectOrderBy } from '../../../../../../shared/ui-chrome-types'
|
||||
import type { WorkspaceStatusDefinition, Worktree } from '../../../../../../shared/worktree/types'
|
||||
import type { AppState } from '../../../../store/types'
|
||||
import { getRepoDisplayLabelKey, getRepoDisplayLabelsByPath } from '@/lib/repo-display-labels'
|
||||
import type { ProjectGroupingModel } from '../../../../../../shared/project-section-grouping'
|
||||
import { ALL_GROUP_KEY } from './group-keys'
|
||||
import type {
|
||||
OrderedGroupEntry,
|
||||
ProjectGroupingModel,
|
||||
WorktreeGroupEntry
|
||||
} from './project-grouping'
|
||||
import type { WorktreeGroupBy } from './row-types'
|
||||
import { getGroupKeyForWorktree } from './worktree-group-keys'
|
||||
|
||||
@@ -73,158 +67,6 @@ export function orderMainWorktreeFirst(worktrees: Worktree[]): Worktree[] {
|
||||
return [...mainWorktrees, ...worktrees.filter((worktree) => !worktree.isMainWorktree)]
|
||||
}
|
||||
|
||||
// Why: disambiguate a section's *own* label, not its anchor checkout's repo
|
||||
// name. Substituting repo.displayName made a project header read the project
|
||||
// name while it was the only repo-backed section and the checkout name as soon
|
||||
// as a second one rendered, so filtering workspaces renamed the project
|
||||
// (#16127). Path suffixes still resolve genuinely identical labels.
|
||||
export function withRepoSectionDisplayLabels(
|
||||
entries: readonly OrderedGroupEntry[]
|
||||
): OrderedGroupEntry[] {
|
||||
const labelItems = entries.flatMap(([, group]) =>
|
||||
group.repo ? [{ ...group.repo, displayName: group.label }] : []
|
||||
)
|
||||
if (labelItems.length === 0) {
|
||||
return [...entries]
|
||||
}
|
||||
const labelsByPath = getRepoDisplayLabelsByPath(labelItems)
|
||||
return entries.map(([key, group]) => [
|
||||
key,
|
||||
group.repo
|
||||
? { ...group, label: labelsByPath.get(getRepoDisplayLabelKey(group.repo)) ?? group.label }
|
||||
: group
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* Recent rank for a project header. `hasActivity` projects (at least one
|
||||
* visible worktree) always sort before fallback projects, regardless of the
|
||||
* numeric values — a placeholder's `addedAt` must never outrank real activity.
|
||||
* Within each tier, higher timestamps come first.
|
||||
*/
|
||||
type RecentRank = { hasActivity: boolean; ts: number }
|
||||
|
||||
export function recentRankForEntry(entry: OrderedGroupEntry): RecentRank {
|
||||
let max = Number.NEGATIVE_INFINITY
|
||||
for (const worktree of entry[1].items) {
|
||||
if (worktree.lastActivityAt > max) {
|
||||
max = worktree.lastActivityAt
|
||||
}
|
||||
}
|
||||
if (max !== Number.NEGATIVE_INFINITY) {
|
||||
// Why: Recent must be timestamp-based, not encounter order — the incoming
|
||||
// array is no longer pre-sorted by recency once decoupled from sortBy.
|
||||
return { hasActivity: true, ts: max }
|
||||
}
|
||||
const addedAt = entry[1].repo?.addedAt
|
||||
return {
|
||||
hasActivity: false,
|
||||
ts: typeof addedAt === 'number' ? addedAt : Number.NEGATIVE_INFINITY
|
||||
}
|
||||
}
|
||||
|
||||
export function createRecentRankLookup(): (entry: OrderedGroupEntry) => RecentRank {
|
||||
const ranks = new Map<OrderedGroupEntry, RecentRank>()
|
||||
return (entry) => {
|
||||
const existing = ranks.get(entry)
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
const rank = recentRankForEntry(entry)
|
||||
ranks.set(entry, rank)
|
||||
return rank
|
||||
}
|
||||
}
|
||||
|
||||
export function compareRecentRank(a: RecentRank, b: RecentRank): number {
|
||||
if (a.hasActivity !== b.hasActivity) {
|
||||
return a.hasActivity ? -1 : 1
|
||||
}
|
||||
return b.ts - a.ts
|
||||
}
|
||||
|
||||
function manualRankForEntry(
|
||||
entry: OrderedGroupEntry,
|
||||
repoOrder: Map<string, number> | undefined
|
||||
): number {
|
||||
const key = entry[0]
|
||||
const repoIds =
|
||||
entry[1].repoIds.size > 0
|
||||
? [...entry[1].repoIds]
|
||||
: [key.startsWith('repo:') ? key.slice('repo:'.length) : key]
|
||||
let rank = Number.POSITIVE_INFINITY
|
||||
for (const repoId of repoIds) {
|
||||
const repoRank = repoOrder?.get(repoId)
|
||||
if (repoRank !== undefined && repoRank < rank) {
|
||||
rank = repoRank
|
||||
}
|
||||
}
|
||||
return rank
|
||||
}
|
||||
|
||||
export function getManualOrderAnchorRepo(
|
||||
group: WorktreeGroupEntry,
|
||||
repoMap: Map<string, Repo>,
|
||||
repoOrder: Map<string, number> | undefined
|
||||
): Repo | undefined {
|
||||
let anchor = group.repo
|
||||
let anchorRank = anchor ? (repoOrder?.get(anchor.id) ?? Number.POSITIVE_INFINITY) : undefined
|
||||
for (const repoId of group.repoIds) {
|
||||
const repo = repoMap.get(repoId)
|
||||
if (!repo) {
|
||||
continue
|
||||
}
|
||||
const rank = repoOrder?.get(repoId) ?? Number.POSITIVE_INFINITY
|
||||
if (!anchor || rank < (anchorRank ?? Number.POSITIVE_INFINITY)) {
|
||||
anchor = repo
|
||||
anchorRank = rank
|
||||
}
|
||||
}
|
||||
return anchor
|
||||
}
|
||||
|
||||
/**
|
||||
* Order project header entries by the user's project-order preference. Manual
|
||||
* follows the canonical repoOrder; Recent follows each project's most recent
|
||||
* visible workspace activity (descending), with empty/imported-only projects
|
||||
* sorting after active ones, then by manual rank, then label.
|
||||
*/
|
||||
export function sortProjectEntries(
|
||||
entries: OrderedGroupEntry[],
|
||||
projectOrderBy: ProjectOrderBy,
|
||||
repoOrder: Map<string, number> | undefined
|
||||
): OrderedGroupEntry[] {
|
||||
if (projectOrderBy === 'recent') {
|
||||
if (entries.length < 2) {
|
||||
return [...entries]
|
||||
}
|
||||
const getRecentRank = createRecentRankLookup()
|
||||
return [...entries].sort((a, b) => {
|
||||
const byRecent = compareRecentRank(getRecentRank(a), getRecentRank(b))
|
||||
if (byRecent !== 0) {
|
||||
return byRecent
|
||||
}
|
||||
const ma = manualRankForEntry(a, repoOrder)
|
||||
const mb = manualRankForEntry(b, repoOrder)
|
||||
if (ma !== mb) {
|
||||
return ma - mb
|
||||
}
|
||||
return a[1].label.localeCompare(b[1].label)
|
||||
})
|
||||
}
|
||||
if (!repoOrder) {
|
||||
return entries
|
||||
}
|
||||
return [...entries].sort((a, b) => {
|
||||
const ra = manualRankForEntry(a, repoOrder)
|
||||
const rb = manualRankForEntry(b, repoOrder)
|
||||
if (ra !== rb) {
|
||||
return ra - rb
|
||||
}
|
||||
return a[1].label.localeCompare(b[1].label)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the flat row list consumed by the virtualizer.
|
||||
* Extracted here to keep WorktreeList.tsx under the line-count lint limit.
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { ProjectSectionEntry } from '../../../../../../shared/project-section-grouping'
|
||||
import type { Worktree } from '../../../../../../shared/worktree/types'
|
||||
import type { RenderableFolderWorkspace } from './folder-workspace-lanes'
|
||||
|
||||
export type OrderedGroupEntry = [string, WorktreeGroupEntry]
|
||||
|
||||
export type WorktreeGroupEntry = ProjectSectionEntry<Worktree> & {
|
||||
/** Folder workspaces bucketed into this lane under non-repo grouping. Carries
|
||||
* the owning group because FolderWorkspaceRow requires a non-optional one. */
|
||||
folderWorkspaces?: RenderableFolderWorkspace[]
|
||||
}
|
||||
@@ -5,8 +5,11 @@ import { getWorkspaceStatus, getWorkspaceStatusGroupKey } from '../../workspace-
|
||||
import { cloneDefaultWorkspaceStatuses } from '../../../../../../shared/workspace-statuses'
|
||||
import type { AppState } from '../../../../store/types'
|
||||
import { ALL_GROUP_KEY, getPRGroupKey, getProjectGroupHeaderKey } from './group-keys'
|
||||
import { buildProjectGroupingIndex, getProjectGroupingForRepo } from './project-grouping'
|
||||
import type { ProjectGroupingModel } from './project-grouping'
|
||||
import {
|
||||
buildProjectGroupingIndex,
|
||||
getProjectGroupingForRepo
|
||||
} from '../../../../../../shared/project-section-grouping'
|
||||
import type { ProjectGroupingModel } from '../../../../../../shared/project-section-grouping'
|
||||
import type { WorktreeGroupBy } from './row-types'
|
||||
|
||||
export function getGroupKeyForWorktree(
|
||||
|
||||
@@ -14,19 +14,18 @@ import {
|
||||
} from './folder-workspace-lanes'
|
||||
import { PR_GROUP_META, PR_GROUP_ORDER, getPRGroupKey, getPRLaneKey } from './group-keys'
|
||||
import type { PRGroupKey } from './group-keys'
|
||||
import { addRepoIdToGroup, getProjectGroupingForRepo } from './project-grouping'
|
||||
import type {
|
||||
OrderedGroupEntry,
|
||||
ProjectGroupingIndex,
|
||||
WorktreeGroupEntry
|
||||
} from './project-grouping'
|
||||
import type { OrderedGroupEntry, WorktreeGroupEntry } from './worktree-group-entry'
|
||||
import {
|
||||
buildProjectSections,
|
||||
type ProjectGroupingIndex
|
||||
} from '../../../../../../shared/project-section-grouping'
|
||||
import { orderProjectSections } from '../../../../../../shared/project-section-order'
|
||||
import type {
|
||||
ImportedWorktreesCardCandidate,
|
||||
NewExternalWorktreesInboxCandidate,
|
||||
PendingCreationRef,
|
||||
WorktreeGroupBy
|
||||
} from './row-types'
|
||||
import { getManualOrderAnchorRepo, sortProjectEntries } from './section-order'
|
||||
|
||||
/** Lane label for a lane a folder workspace opened before any worktree did. */
|
||||
function getLaneLabelForKey(
|
||||
@@ -78,17 +77,33 @@ export function buildOrderedGroups(args: {
|
||||
folderWorkspaces = []
|
||||
} = args
|
||||
|
||||
if (groupBy === 'repo') {
|
||||
// Why shared: the paired phone builds its Project sections with this same function.
|
||||
return buildProjectSections({
|
||||
items: naturalWorktrees,
|
||||
repoIdOf: (worktree) => worktree.repoId,
|
||||
repoMap,
|
||||
projectIndex,
|
||||
placeholderRepoIds,
|
||||
noticeRepos: [
|
||||
...[...importedWorktreesByRepo].map(
|
||||
([repoId, candidate]) => [repoId, candidate.repo] as const
|
||||
),
|
||||
...[...newExternalWorktreesInboxByRepo].map(
|
||||
([repoId, candidate]) => [repoId, candidate.repo] as const
|
||||
)
|
||||
],
|
||||
pendingRepoIds: pendingByRepo.keys(),
|
||||
repoOrder,
|
||||
projectOrderBy
|
||||
})
|
||||
}
|
||||
|
||||
const grouped = new Map<string, WorktreeGroupEntry>()
|
||||
for (const w of naturalWorktrees) {
|
||||
let key: string
|
||||
let label: string
|
||||
let repo: Repo | undefined
|
||||
if (groupBy === 'repo') {
|
||||
const grouping = getProjectGroupingForRepo(w.repoId, repoMap, projectIndex)
|
||||
key = grouping.key
|
||||
label = grouping.label
|
||||
repo = grouping.repo
|
||||
} else if (groupBy === 'workspace-status') {
|
||||
if (groupBy === 'workspace-status') {
|
||||
const workspaceStatus = getWorkspaceStatus(w, workspaceStatuses)
|
||||
key = getWorkspaceStatusGroupKey(workspaceStatus)
|
||||
label =
|
||||
@@ -99,111 +114,34 @@ export function buildOrderedGroups(args: {
|
||||
label = PR_GROUP_META[prGroup].label
|
||||
}
|
||||
if (!grouped.has(key)) {
|
||||
grouped.set(key, { label, items: [], repo, repoIds: new Set() })
|
||||
grouped.set(key, { label, items: [], repo: undefined, repoIds: new Set() })
|
||||
}
|
||||
const group = grouped.get(key)!
|
||||
group.items.push(w)
|
||||
addRepoIdToGroup(group, w.repoId)
|
||||
group.repoIds.add(w.repoId)
|
||||
}
|
||||
// Why: folder workspaces are not worktrees, so they never appear in the loop
|
||||
// above. Bucketing them here — and creating the lane when no worktree opened
|
||||
// one — is what lets a folder workspace be the sole occupant of a lane (#15362).
|
||||
if (groupBy !== 'repo') {
|
||||
for (const pair of folderWorkspaces) {
|
||||
const key = getFolderWorkspaceLaneKey(pair, groupBy, workspaceStatuses)
|
||||
if (!grouped.has(key)) {
|
||||
grouped.set(key, {
|
||||
label: getLaneLabelForKey(key, groupBy, workspaceStatuses),
|
||||
items: [],
|
||||
repo: undefined,
|
||||
repoIds: new Set()
|
||||
})
|
||||
}
|
||||
const group = grouped.get(key)!
|
||||
group.folderWorkspaces ??= []
|
||||
group.folderWorkspaces.push(pair)
|
||||
}
|
||||
for (const group of grouped.values()) {
|
||||
group.folderWorkspaces?.sort((left, right) =>
|
||||
compareFolderWorkspacesForDisplay(left.folderWorkspace, right.folderWorkspace)
|
||||
)
|
||||
for (const pair of folderWorkspaces) {
|
||||
const key = getFolderWorkspaceLaneKey(pair, groupBy, workspaceStatuses)
|
||||
if (!grouped.has(key)) {
|
||||
grouped.set(key, {
|
||||
label: getLaneLabelForKey(key, groupBy, workspaceStatuses),
|
||||
items: [],
|
||||
repo: undefined,
|
||||
repoIds: new Set()
|
||||
})
|
||||
}
|
||||
const group = grouped.get(key)!
|
||||
group.folderWorkspaces ??= []
|
||||
group.folderWorkspaces.push(pair)
|
||||
}
|
||||
if (groupBy === 'repo') {
|
||||
for (const repoId of placeholderRepoIds) {
|
||||
const grouping = getProjectGroupingForRepo(repoId, repoMap, projectIndex)
|
||||
if (!grouping.repo) {
|
||||
continue
|
||||
}
|
||||
const key = grouping.key
|
||||
if (!grouped.has(key)) {
|
||||
// Why: repos can arrive before worktree scans, but stale IDs passed by
|
||||
// older snapshots must not render an "Unknown" project header.
|
||||
grouped.set(key, {
|
||||
label: grouping.label,
|
||||
items: [],
|
||||
repo: grouping.repo,
|
||||
repoIds: new Set([repoId])
|
||||
})
|
||||
} else {
|
||||
addRepoIdToGroup(grouped.get(key)!, repoId)
|
||||
}
|
||||
}
|
||||
for (const group of grouped.values()) {
|
||||
group.folderWorkspaces?.sort((left, right) =>
|
||||
compareFolderWorkspacesForDisplay(left.folderWorkspace, right.folderWorkspace)
|
||||
)
|
||||
}
|
||||
if (groupBy === 'repo') {
|
||||
for (const [repoId, candidate] of importedWorktreesByRepo) {
|
||||
const grouping = getProjectGroupingForRepo(repoId, repoMap, projectIndex)
|
||||
const key = grouping.key
|
||||
if (!grouped.has(key)) {
|
||||
grouped.set(key, {
|
||||
label: grouping.label,
|
||||
items: [],
|
||||
repo: grouping.repo ?? candidate.repo,
|
||||
repoIds: new Set([repoId])
|
||||
})
|
||||
} else if (grouped.has(key)) {
|
||||
addRepoIdToGroup(grouped.get(key)!, repoId)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (groupBy === 'repo') {
|
||||
for (const [repoId, candidate] of newExternalWorktreesInboxByRepo) {
|
||||
const grouping = getProjectGroupingForRepo(repoId, repoMap, projectIndex)
|
||||
const key = grouping.key
|
||||
if (!grouped.has(key)) {
|
||||
// Why: the default policy removes pinned worktrees from natural groups,
|
||||
// but actionable inbox rows still need a project section to render in.
|
||||
grouped.set(key, {
|
||||
label: grouping.label,
|
||||
items: [],
|
||||
repo: grouping.repo ?? candidate.repo,
|
||||
repoIds: new Set([repoId])
|
||||
})
|
||||
} else if (grouped.has(key)) {
|
||||
addRepoIdToGroup(grouped.get(key)!, repoId)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (groupBy === 'repo') {
|
||||
for (const repoId of pendingByRepo.keys()) {
|
||||
const grouping = getProjectGroupingForRepo(repoId, repoMap, projectIndex)
|
||||
const key = grouping.key
|
||||
if (!grouped.has(key)) {
|
||||
// Why: creating the first worktree in a repo leaves it with no group yet;
|
||||
// ensure one so the in-progress row nests under its repo instead of being
|
||||
// dropped.
|
||||
grouped.set(key, {
|
||||
label: grouping.label,
|
||||
items: [],
|
||||
repo: grouping.repo,
|
||||
repoIds: new Set([repoId])
|
||||
})
|
||||
} else {
|
||||
addRepoIdToGroup(grouped.get(key)!, repoId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const orderedGroups: OrderedGroupEntry[] = []
|
||||
if (groupBy === 'pr-status') {
|
||||
for (const prGroup of PR_GROUP_ORDER) {
|
||||
@@ -224,16 +162,7 @@ export function buildOrderedGroups(args: {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const group of grouped.values()) {
|
||||
// Why: logical project headers can contain multiple host setup repos.
|
||||
// Use the repo that anchors manual order so drag and actions target the
|
||||
// same persisted order source the row sorter reads.
|
||||
group.repo = getManualOrderAnchorRepo(group, repoMap, repoOrder)
|
||||
}
|
||||
// Why: project header order is its own user choice (projectOrderBy),
|
||||
// decoupled from workspace sortBy. Manual uses the canonical repoOrder so
|
||||
// header drag has a stable source of truth; Recent follows activity.
|
||||
const entries = sortProjectEntries(Array.from(grouped.entries()), projectOrderBy, repoOrder)
|
||||
const entries = orderProjectSections(grouped, repoMap, repoOrder, projectOrderBy)
|
||||
// Why: large imported repo sets can have one group per repo; spreading
|
||||
// those entries into push can exceed V8's argument limit.
|
||||
for (const entry of entries) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { Repo } from '../../../../../../shared/repo-types'
|
||||
import type { WorkspaceStatusDefinition, Worktree } from '../../../../../../shared/worktree/types'
|
||||
import type { WorktreeLineage } from '../../../../../../shared/worktree/lineage-types'
|
||||
import type { PinnedWorktreeDisplayPolicy, WorktreeGroupBy } from '../grouping/row-types'
|
||||
import type { ProjectGroupingModel } from '../grouping/project-grouping'
|
||||
import type { ProjectGroupingModel } from '../../../../../../shared/project-section-grouping'
|
||||
import { getFolderWorkspaceRevealGroupKeys } from '../navigation/folder-reveal'
|
||||
import type { FolderWorkspace } from '../../../../../../shared/folder-workspace-types'
|
||||
import type { ExecutionHostId } from '../../../../../../shared/execution-host'
|
||||
|
||||
@@ -13,9 +13,9 @@ import { folderWorkspaceKey } from '../../../../../../shared/workspace-scope'
|
||||
import { getFolderWorkspaceHostIdentity } from '../../../../../../shared/folder-workspace-worktree'
|
||||
import { getHostDisplayLabelOverrides } from '../../../../../../shared/host-setting-overrides'
|
||||
import { buildRows } from '../grouping/build-rows'
|
||||
import type { ProjectGroupingModel } from '../grouping/project-grouping'
|
||||
import type { ProjectGroupingModel } from '../../../../../../shared/project-section-grouping'
|
||||
import type { PinnedWorktreeDisplayPolicy, Row, WorktreeGroupBy } from '../grouping/row-types'
|
||||
import { getLogicalRepoOrderRankById } from '../../project-header-drop'
|
||||
import { getLogicalRepoOrderRankById } from '../../../../../../shared/project-section-order'
|
||||
import { getEmptyProjectPlaceholderRepoIds } from '../../empty-project-placeholder-repos'
|
||||
import { deferHostSectionCollapse, scopeHostSectionCollapse } from '../../host-section-collapse'
|
||||
import { addHostSectionRows } from '../../host-section-rows'
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import type { WorkspaceStatusDefinition, Worktree } from '../../../../../../shar
|
||||
import type { WorktreeLineage } from '../../../../../../shared/worktree/lineage-types'
|
||||
import type { ExecutionHostId } from '../../../../../../shared/execution-host'
|
||||
import type { RenderRow } from '../listing/render-row'
|
||||
import type { ProjectGroupingModel } from '../grouping/project-grouping'
|
||||
import type { ProjectGroupingModel } from '../../../../../../shared/project-section-grouping'
|
||||
import type { PinnedWorktreeDisplayPolicy, WorktreeGroupBy } from '../grouping/row-types'
|
||||
import { getFolderWorkspaceRevealGroupKeys } from './folder-reveal'
|
||||
import { getWorktreeRevealCollapsedGroupKeys } from './worktree-reveal-group-keys'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ProjectGroup } from '../../../../../../shared/project-group-types'
|
||||
import type { Repo } from '../../../../../../shared/repo-types'
|
||||
import { getProjectGroupHeaderKey } from '../grouping/group-keys'
|
||||
import type { ProjectGroupingModel } from '../grouping/project-grouping'
|
||||
import type { ProjectGroupingModel } from '../../../../../../shared/project-section-grouping'
|
||||
|
||||
function getProjectIdFromHeaderRowKey(rowKey: string): string | null {
|
||||
if (!rowKey.startsWith('project:')) {
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import {
|
||||
getWorktreeLineageAncestors
|
||||
} from '../../worktree-lineage-projection'
|
||||
import { getWorktreeLineageGroupKey, PINNED_GROUP_KEY } from '../grouping/group-keys'
|
||||
import type { ProjectGroupingModel } from '../grouping/project-grouping'
|
||||
import type { ProjectGroupingModel } from '../../../../../../shared/project-section-grouping'
|
||||
import type { PinnedWorktreeDisplayPolicy, WorktreeGroupBy } from '../grouping/row-types'
|
||||
import { getGroupKeysForWorktree } from '../grouping/worktree-group-keys'
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import type { ImportedWorktreeCardActionState } from '../../imported-worktrees-c
|
||||
import type { NewExternalWorktreesInboxActionState } from '../../new-external-worktrees-inbox-actions'
|
||||
import type { WorktreeDragGroup } from '../../worktree-manual-order'
|
||||
import type { WorktreeStatusDropAtIndexArgs } from '../drag/drop-commit-context'
|
||||
import type { ProjectGroupingModel } from '../grouping/project-grouping'
|
||||
import type { ProjectGroupingModel } from '../../../../../../shared/project-section-grouping'
|
||||
import type { PinnedWorktreeDisplayPolicy, WorktreeGroupBy } from '../grouping/row-types'
|
||||
|
||||
export const EMPTY_PROJECT_GROUPS: readonly ProjectGroup[] = []
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ProjectGroupingModel } from '@/components/sidebar/worktree-list/grouping/project-grouping'
|
||||
import type { ProjectGroupingModel } from '../../../shared/project-section-grouping'
|
||||
import type { Repo } from '../../../shared/repo-types'
|
||||
|
||||
export type SettingsNavigationBuildOptions = {
|
||||
|
||||
@@ -22,7 +22,7 @@ import { defaultScopeHost } from '@/lib/default-creation-host'
|
||||
import { useAppStore } from '@/store'
|
||||
import { useProjectHostSetupProjection } from '@/store/selectors'
|
||||
import { useStructuredAgentSessionHostQueuesMessages } from '@/runtime/structured-agent-session-host-capability'
|
||||
import type { ProjectGroupingModel } from '@/components/sidebar/worktree-list/grouping/project-grouping'
|
||||
import type { ProjectGroupingModel } from '../../../shared/project-section-grouping'
|
||||
import type { Repo } from '../../../shared/repo-types'
|
||||
import {
|
||||
buildCapabilitySettingsSections,
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
scheduleRuntimeGraphSync,
|
||||
setRuntimeGraphStoreStateGetter,
|
||||
setRuntimeGraphSyncEnabled
|
||||
} from './sync-runtime-graph'
|
||||
import type { AppState } from '../store/types'
|
||||
import type { Repo } from '../../../shared/repo-types'
|
||||
import type { RuntimeSyncWindowGraph } from '../../../shared/runtime-types'
|
||||
|
||||
vi.mock('./sync-runtime-graph/terminal-group-grid-sizes', () => ({
|
||||
measureTerminalGroupGridSizes: () => undefined
|
||||
}))
|
||||
|
||||
const repo: Repo = {
|
||||
id: 'repo-1',
|
||||
path: '/Users/me/orca',
|
||||
displayName: 'orca',
|
||||
badgeColor: '#737373',
|
||||
addedAt: 1,
|
||||
hookSettings: { mode: 'auto', scripts: { setup: 'secret-setup', archive: '' } },
|
||||
gitUsername: 'me',
|
||||
repoIcon: { type: 'image', src: 'data:image/png;base64,AAAA', source: 'upload' }
|
||||
}
|
||||
|
||||
const BASE_STATE = {
|
||||
tabsByWorktree: {},
|
||||
terminalLayoutsByTabId: {},
|
||||
runtimePaneTitlesByTabId: {},
|
||||
groupsByWorktree: {},
|
||||
activeGroupIdByWorktree: {},
|
||||
layoutByWorktree: {},
|
||||
unifiedTabsByWorktree: {},
|
||||
tabBarOrderByWorktree: {},
|
||||
activeFileId: null,
|
||||
activeFileIdByWorktree: {},
|
||||
openFiles: [],
|
||||
editorDrafts: {},
|
||||
activeTabId: null,
|
||||
agentStatusByPaneKey: {},
|
||||
browserTabsByWorktree: {},
|
||||
repos: [repo],
|
||||
projects: [],
|
||||
projectHostSetups: [],
|
||||
projectOrderBy: 'recent'
|
||||
}
|
||||
|
||||
async function flushSync(): Promise<void> {
|
||||
await vi.advanceTimersByTimeAsync(20)
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
setRuntimeGraphSyncEnabled(false)
|
||||
setRuntimeGraphStoreStateGetter(null)
|
||||
vi.useRealTimers()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
describe("the sidebar's Project catalog in the window graph", () => {
|
||||
it('carries it once per change, without repo hooks, credentials or uploaded icons', async () => {
|
||||
const syncWindowGraph = vi
|
||||
.fn<(graph: RuntimeSyncWindowGraph) => Promise<object>>()
|
||||
.mockResolvedValue({})
|
||||
vi.useFakeTimers()
|
||||
vi.stubGlobal('window', { api: { runtime: { syncWindowGraph } } })
|
||||
vi.stubGlobal('HTMLElement', class HTMLElement {})
|
||||
let state = BASE_STATE
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: graph sync reads only these members of the store.
|
||||
setRuntimeGraphStoreStateGetter(() => state as unknown as AppState)
|
||||
setRuntimeGraphSyncEnabled(true)
|
||||
await flushSync()
|
||||
|
||||
expect(syncWindowGraph.mock.calls[0]?.[0].projectSectionCatalog).toEqual({
|
||||
repos: [
|
||||
{
|
||||
id: 'repo-1',
|
||||
displayName: 'orca',
|
||||
path: '/Users/me/orca',
|
||||
badgeColor: '#737373',
|
||||
addedAt: 1
|
||||
}
|
||||
],
|
||||
projects: [{ id: 'repo:repo-1', displayName: 'orca' }],
|
||||
setups: [
|
||||
{
|
||||
id: 'repo-1',
|
||||
projectId: 'repo:repo-1',
|
||||
hostId: 'local',
|
||||
repoId: 'repo-1',
|
||||
path: '/Users/me/orca',
|
||||
displayName: 'orca',
|
||||
setupMethod: 'legacy-repo'
|
||||
}
|
||||
],
|
||||
projectOrderBy: 'recent'
|
||||
})
|
||||
|
||||
scheduleRuntimeGraphSync()
|
||||
await flushSync()
|
||||
expect(syncWindowGraph).toHaveBeenCalledTimes(2)
|
||||
expect(syncWindowGraph.mock.calls[1]?.[0]).not.toHaveProperty('projectSectionCatalog')
|
||||
|
||||
state = { ...BASE_STATE, projectOrderBy: 'manual' }
|
||||
scheduleRuntimeGraphSync()
|
||||
await flushSync()
|
||||
expect(syncWindowGraph.mock.calls[2]?.[0].projectSectionCatalog?.projectOrderBy).toBe('manual')
|
||||
})
|
||||
})
|
||||
@@ -18,6 +18,7 @@ import { buildMobileSessionTabSnapshots } from './mobile-session-snapshots'
|
||||
import { isWebOnlyMirroredTerminalTab } from './mobile-session-surfaces'
|
||||
import { resolveRuntimeTerminalTitle } from './sync-projections'
|
||||
import { measureTerminalGroupGridSizes } from './terminal-group-grid-sizes'
|
||||
import { getProjectSectionCatalogFromState } from './project-section-catalog'
|
||||
import {
|
||||
collectAmbiguousTerminalTabIds,
|
||||
graphState,
|
||||
@@ -60,6 +61,7 @@ export async function syncRuntimeGraph(): Promise<void> {
|
||||
)
|
||||
const publication = partitionMobileSessionPublication(mobileSessionTabs)
|
||||
const gridSizes = measureTerminalGroupGridSizes(state.settings?.terminalFontSize ?? 14)
|
||||
const projectSectionCatalog = getProjectSectionCatalogFromState(state)
|
||||
const graph: RuntimeRendererSyncWindowGraph = {
|
||||
tabs: [],
|
||||
leaves: [],
|
||||
@@ -69,6 +71,9 @@ export async function syncRuntimeGraph(): Promise<void> {
|
||||
...(gridSizes ? { terminalGroupGridSizes: gridSizes.groups } : {}),
|
||||
...(gridSizes?.singleGroupWorkspace
|
||||
? { singleGroupWorkspaceGridSize: gridSizes.singleGroupWorkspace }
|
||||
: {}),
|
||||
...(projectSectionCatalog && projectSectionCatalog !== graphState.publishedProjectSectionCatalog
|
||||
? { projectSectionCatalog }
|
||||
: {})
|
||||
}
|
||||
|
||||
@@ -198,6 +203,8 @@ export async function syncRuntimeGraph(): Promise<void> {
|
||||
try {
|
||||
const result = await window.api.runtime.syncWindowGraph(graph)
|
||||
graphState.publishedTerminalGroupGridSizesKey = JSON.stringify(gridSizes ?? null)
|
||||
graphState.publishedProjectSectionCatalog =
|
||||
projectSectionCatalog ?? graphState.publishedProjectSectionCatalog
|
||||
commitMobileSessionPublication(mobileSessionTabs, result?.mobileSessionResyncWorktrees)
|
||||
const currentState = graphState.getStoreState()
|
||||
currentState?.setRuntimeAgentOrchestrationByPaneKey?.(result?.agentOrchestrationByPaneKey ?? {})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { MobileProjectSectionCatalog } from '../../../../shared/mobile-project-sections-contract'
|
||||
import { createBrowserUuid } from '@/lib/browser-uuid'
|
||||
import type { AppState } from '@/store/types'
|
||||
import type {
|
||||
@@ -31,6 +32,10 @@ export const EMPTY_WORKTREE_OPEN_FILE_IDS: readonly string[] = []
|
||||
|
||||
export const mobilePublicationEpoch = `renderer:${createBrowserUuid()}`
|
||||
|
||||
function noPublishedProjectSectionCatalog(): MobileProjectSectionCatalog | null {
|
||||
return null
|
||||
}
|
||||
|
||||
export const graphState = {
|
||||
syncScheduled: false,
|
||||
syncInFlight: false,
|
||||
@@ -56,7 +61,9 @@ export const graphState = {
|
||||
cachedEditorDraftHashes: null as EditorDraftHashCache | null,
|
||||
mobileSessionSnapshotVersion: 0,
|
||||
/** Last group grid sizes main accepted, so a refit republishes only when one changed. */
|
||||
publishedTerminalGroupGridSizesKey: ''
|
||||
publishedTerminalGroupGridSizesKey: '',
|
||||
/** Last sidebar Project catalog main accepted, so the graph carries it only when it changed. */
|
||||
publishedProjectSectionCatalog: noPublishedProjectSectionCatalog()
|
||||
}
|
||||
|
||||
/** IDs occurring more than once cannot address the legacy tab-keyed runtime maps safely. */
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
buildMobileProjectSectionCatalog,
|
||||
type MobileProjectSectionCatalog
|
||||
} from '../../../../shared/mobile-project-sections-contract'
|
||||
import type { ProjectHostSetupProjection } from '../../../../shared/project-host-setup-projection'
|
||||
import { getProjectHostSetupProjectionFromState } from '@/store/project-host-setup-selector'
|
||||
import type { AppState } from '@/store/types'
|
||||
|
||||
type CatalogSource = {
|
||||
repos: AppState['repos']
|
||||
projection: ProjectHostSetupProjection
|
||||
projectOrderBy: AppState['projectOrderBy']
|
||||
catalog: MobileProjectSectionCatalog
|
||||
}
|
||||
|
||||
let cachedCatalogSource: CatalogSource | null = null
|
||||
|
||||
/** What the sidebar groups Project headers by: state.repos in its order and the selector's projection. */
|
||||
export function getProjectSectionCatalogFromState(
|
||||
state: Pick<AppState, 'repos' | 'projects' | 'projectHostSetups' | 'projectOrderBy'>
|
||||
): MobileProjectSectionCatalog | null {
|
||||
// Why: graph publication must not fail on a store slice that holds no repo catalog.
|
||||
if (!Array.isArray(state.repos)) {
|
||||
return null
|
||||
}
|
||||
const projection = getProjectHostSetupProjectionFromState(state)
|
||||
const cached = cachedCatalogSource
|
||||
if (
|
||||
cached &&
|
||||
cached.repos === state.repos &&
|
||||
cached.projection === projection &&
|
||||
cached.projectOrderBy === state.projectOrderBy
|
||||
) {
|
||||
return cached.catalog
|
||||
}
|
||||
const catalog = buildMobileProjectSectionCatalog(state.repos, projection, state.projectOrderBy)
|
||||
cachedCatalogSource = {
|
||||
repos: state.repos,
|
||||
projection,
|
||||
projectOrderBy: state.projectOrderBy,
|
||||
catalog
|
||||
}
|
||||
return catalog
|
||||
}
|
||||
@@ -8,12 +8,14 @@ import type { ProjectGroup } from '../../../shared/project-group-types'
|
||||
import type { ProjectHostSetup } from '../../../shared/project-types'
|
||||
import type { Repo } from '../../../shared/repo-types'
|
||||
import {
|
||||
getRepoExecutionHostId,
|
||||
LOCAL_EXECUTION_HOST_ID,
|
||||
toSshExecutionHostId,
|
||||
type ExecutionHostId
|
||||
} from '../../../shared/execution-host'
|
||||
import { normalizeProjectHostSetupRow } from '../../../shared/project-catalog-row-normalization'
|
||||
import {
|
||||
adoptEndpointProjectHostSetup,
|
||||
adoptEndpointRepo
|
||||
} from '../../../shared/endpoint-catalog-adoption'
|
||||
import type { RuntimeClientTarget } from '../../../shared/runtime-client-target'
|
||||
import { getRuntimeTargetHostId } from '../runtime/runtime-client-target'
|
||||
|
||||
@@ -81,11 +83,11 @@ export function adoptFromEndpoint(
|
||||
): Repo | ProjectGroup | ProjectHostSetup | FolderWorkspace | OwnedWorktreeEvent | ExecutionHostId {
|
||||
switch (fetched.kind) {
|
||||
case 'repo':
|
||||
return adoptRepo(target, fetched.row)
|
||||
return adoptEndpointRepo(target, fetched.row)
|
||||
case 'projectGroup':
|
||||
return adoptProjectGroup(target, fetched.row)
|
||||
case 'projectHostSetup':
|
||||
return adoptProjectHostSetup(target, fetched.row)
|
||||
return adoptEndpointProjectHostSetup(target, fetched.row)
|
||||
case 'folderWorkspace':
|
||||
return {
|
||||
...fetched.row,
|
||||
@@ -110,17 +112,6 @@ export function adoptFromEndpoint(
|
||||
}
|
||||
}
|
||||
|
||||
function adoptRepo(target: RuntimeClientTarget, repo: Repo): Repo {
|
||||
if (target.kind === 'environment') {
|
||||
// `connectionId` survives: on a server's row it names that server's nested SSH target.
|
||||
return { ...repo, executionHostId: getRuntimeTargetHostId(target) }
|
||||
}
|
||||
if (repo.connectionId) {
|
||||
return { ...repo, executionHostId: getRepoExecutionHostId(repo) }
|
||||
}
|
||||
return repo.executionHostId ? repo : { ...repo, executionHostId: LOCAL_EXECUTION_HOST_ID }
|
||||
}
|
||||
|
||||
function adoptProjectGroup(target: RuntimeClientTarget, projectGroup: ProjectGroup): ProjectGroup {
|
||||
if (target.kind === 'environment') {
|
||||
return { ...projectGroup, executionHostId: getRuntimeTargetHostId(target) }
|
||||
@@ -130,25 +121,3 @@ function adoptProjectGroup(target: RuntimeClientTarget, projectGroup: ProjectGro
|
||||
}
|
||||
return { ...projectGroup, executionHostId: LOCAL_EXECUTION_HOST_ID }
|
||||
}
|
||||
|
||||
function adoptProjectHostSetup(
|
||||
target: RuntimeClientTarget,
|
||||
setup: ProjectHostSetup
|
||||
): ProjectHostSetup {
|
||||
// Why here: every setup row crossing IPC or RPC into the renderer passes through this
|
||||
// adoption step, so it is where the declared field types stop being aspirational.
|
||||
const adopted = normalizeProjectHostSetupRow(setup)
|
||||
if (target.kind !== 'environment') {
|
||||
return adopted
|
||||
}
|
||||
const hostId = getRuntimeTargetHostId(target)
|
||||
const executionHostId = adopted.executionHostId ?? adopted.hostId
|
||||
return {
|
||||
...adopted,
|
||||
hostId,
|
||||
executionHostId: executionHostId === LOCAL_EXECUTION_HOST_ID ? hostId : executionHostId,
|
||||
runtimeOwnerEnvironmentId: target.environmentId,
|
||||
// Why: paired clients route through the HUB and must not treat its private SSH target as client-local configuration.
|
||||
connectionId: null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { WorkItemsCacheError } from './cache-model'
|
||||
import { isGitHubWorkItemsSshRemoteRequiredError } from '../../../../shared/work-items'
|
||||
import { getTaskSourceCacheScope } from '../../../../shared/task-source-context'
|
||||
import { isGitHubWorkItemsQueryTooLarge } from '../slices/github-work-items-query-bounds'
|
||||
import { reconcileCatalogRows } from '../slices/repo-identity-reconcile'
|
||||
import { reconcileCatalogRows } from '../../../../shared/catalog-row-reconcile'
|
||||
import { structuralValuesEqual } from '../../../../shared/structural-value-equality'
|
||||
import { workItemsCacheKey, workItemsInflightRequestKey } from './cache-identity'
|
||||
import { isFresh, withBoundedCacheEntry, WORK_ITEMS_CACHE_TTL } from './cache-policy'
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import type { Project, ProjectHostSetup } from '../../../shared/project-types'
|
||||
import type { Repo } from '../../../shared/repo-types'
|
||||
import {
|
||||
getProjectIdentityKey,
|
||||
type ProjectHostSetupProjection
|
||||
} from '../../../shared/project-host-setup-projection'
|
||||
|
||||
export type NormalizedProjectHostSetupProjection = ProjectHostSetupProjection & {
|
||||
changed: boolean
|
||||
}
|
||||
|
||||
export function normalizeHydratedProjectHostSetupProjection(
|
||||
repos: readonly Repo[],
|
||||
projects: readonly Project[],
|
||||
setups: readonly ProjectHostSetup[],
|
||||
derived: ProjectHostSetupProjection
|
||||
): NormalizedProjectHostSetupProjection {
|
||||
const repoById = new Map(repos.map((repo) => [repo.id, repo]))
|
||||
const derivedProjectIds = new Set(derived.projects.map((project) => project.id))
|
||||
const projectIdByHydratedProjectId = new Map<string, string>()
|
||||
let changed = false
|
||||
const normalizedSetups = setups.map((setup) => {
|
||||
const repo = repoById.get(setup.repoId) ?? repoById.get(setup.id)
|
||||
if (!repo) {
|
||||
return setup
|
||||
}
|
||||
const projectId = getProjectIdentityKey(repo)
|
||||
if (projectId === setup.projectId || projectId === `repo:${repo.id}`) {
|
||||
return setup
|
||||
}
|
||||
changed = true
|
||||
projectIdByHydratedProjectId.set(setup.projectId, projectId)
|
||||
return { ...setup, projectId }
|
||||
})
|
||||
const normalizedProjects = projects.flatMap((project) => {
|
||||
const projectId = projectIdByHydratedProjectId.get(project.id)
|
||||
if (!projectId || projectId === project.id) {
|
||||
return [project]
|
||||
}
|
||||
// Why: runtime-hosted copies of the same Git repo may hydrate path-scoped
|
||||
// project ids. If the repo-derived project already exists, keep that bucket
|
||||
// authoritative so VM copies group under the user's single project.
|
||||
if (derivedProjectIds.has(projectId)) {
|
||||
changed = true
|
||||
return []
|
||||
}
|
||||
changed = true
|
||||
return [{ ...project, id: projectId }]
|
||||
})
|
||||
return { projects: normalizedProjects, setups: normalizedSetups, changed }
|
||||
}
|
||||
@@ -3,226 +3,55 @@ import {
|
||||
projectHostSetupProjectionFromRepos,
|
||||
type ProjectHostSetupProjection
|
||||
} from '../../../shared/project-host-setup-projection'
|
||||
import { resolveProjectHostSetupProjection } from '../../../shared/project-host-setup-hydration'
|
||||
import type { AppState } from './types'
|
||||
import { normalizeHydratedProjectHostSetupProjection } from './project-host-setup-selector-normalization'
|
||||
|
||||
const projectHostSetupProjectionCache = new WeakMap<AppState['repos'], ProjectHostSetupProjection>()
|
||||
const providedProjectHostSetupProjectionCache = new WeakMap<
|
||||
Project[],
|
||||
WeakMap<ProjectHostSetup[], ProjectHostSetupProjection>
|
||||
>()
|
||||
const mergedProjectHostSetupProjectionCache = new WeakMap<
|
||||
AppState['repos'],
|
||||
WeakMap<Project[], WeakMap<ProjectHostSetup[], ProjectHostSetupProjection>>
|
||||
>()
|
||||
const normalizedProjectHostSetupProjectionCache = new WeakMap<
|
||||
AppState['repos'],
|
||||
WeakMap<Project[], WeakMap<ProjectHostSetup[], ProjectHostSetupProjection>>
|
||||
>()
|
||||
const derivedProjectionByRepos = new WeakMap<AppState['repos'], ProjectHostSetupProjection>()
|
||||
// Catalog writers replace these readonly arrays; their identities are the invalidation boundary.
|
||||
type StateProjectHostSetupProjectionCache = {
|
||||
repos: AppState['repos']
|
||||
projects: AppState['projects']
|
||||
setups: AppState['projectHostSetups']
|
||||
projection: ProjectHostSetupProjection
|
||||
}
|
||||
const resolvedProjectionByCatalog = new WeakMap<
|
||||
AppState['repos'],
|
||||
WeakMap<readonly Project[], WeakMap<readonly ProjectHostSetup[], ProjectHostSetupProjection>>
|
||||
>()
|
||||
|
||||
let stateProjectHostSetupProjectionCache: StateProjectHostSetupProjectionCache | null = null
|
||||
|
||||
function getCachedStateProjectHostSetupProjection(
|
||||
repos: AppState['repos'],
|
||||
projects: AppState['projects'],
|
||||
setups: AppState['projectHostSetups']
|
||||
): ProjectHostSetupProjection | undefined {
|
||||
const cached = stateProjectHostSetupProjectionCache
|
||||
return cached &&
|
||||
cached.repos === repos &&
|
||||
cached.projects === projects &&
|
||||
cached.setups === setups
|
||||
? cached.projection
|
||||
: undefined
|
||||
}
|
||||
|
||||
function cacheStateProjectHostSetupProjection(
|
||||
repos: AppState['repos'],
|
||||
projects: AppState['projects'],
|
||||
setups: AppState['projectHostSetups'],
|
||||
projection: ProjectHostSetupProjection
|
||||
): void {
|
||||
stateProjectHostSetupProjectionCache = { repos, projects, setups, projection }
|
||||
}
|
||||
|
||||
function getCachedProjectHostSetupProjection(repos: AppState['repos']): ProjectHostSetupProjection {
|
||||
const cachedProjection = projectHostSetupProjectionCache.get(repos)
|
||||
if (cachedProjection) {
|
||||
return cachedProjection
|
||||
function getDerivedProjection(repos: AppState['repos']): ProjectHostSetupProjection {
|
||||
const cached = derivedProjectionByRepos.get(repos)
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
|
||||
const projection = projectHostSetupProjectionFromRepos(repos)
|
||||
projectHostSetupProjectionCache.set(repos, projection)
|
||||
return projection
|
||||
}
|
||||
|
||||
function getCachedProvidedProjectHostSetupProjection(
|
||||
projects: Project[],
|
||||
setups: ProjectHostSetup[]
|
||||
): ProjectHostSetupProjection {
|
||||
const cachedBySetups = providedProjectHostSetupProjectionCache.get(projects)
|
||||
const cachedProjection = cachedBySetups?.get(setups)
|
||||
if (cachedProjection) {
|
||||
return cachedProjection
|
||||
}
|
||||
|
||||
const projection = { projects, setups }
|
||||
const nextCachedBySetups =
|
||||
cachedBySetups ?? new WeakMap<ProjectHostSetup[], ProjectHostSetupProjection>()
|
||||
nextCachedBySetups.set(setups, projection)
|
||||
if (!cachedBySetups) {
|
||||
providedProjectHostSetupProjectionCache.set(projects, nextCachedBySetups)
|
||||
}
|
||||
return projection
|
||||
}
|
||||
|
||||
function mergeById<T extends { id: string }>(base: readonly T[], overlay: readonly T[]): T[] {
|
||||
const merged = [...base]
|
||||
const indexById = new Map(merged.map((entry, index) => [entry.id, index]))
|
||||
for (const entry of overlay) {
|
||||
const index = indexById.get(entry.id)
|
||||
if (index === undefined) {
|
||||
indexById.set(entry.id, merged.length)
|
||||
merged.push(entry)
|
||||
} else {
|
||||
merged[index] = entry
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
function mergeProjectHostSetupProjection(
|
||||
repos: AppState['repos'],
|
||||
projects: Project[],
|
||||
setups: ProjectHostSetup[]
|
||||
): ProjectHostSetupProjection {
|
||||
const cachedByProjects = mergedProjectHostSetupProjectionCache.get(repos)
|
||||
const cachedBySetups = cachedByProjects?.get(projects)
|
||||
const cachedProjection = cachedBySetups?.get(setups)
|
||||
if (cachedProjection) {
|
||||
return cachedProjection
|
||||
}
|
||||
const derived = getCachedProjectHostSetupProjection(repos)
|
||||
const normalized = normalizeHydratedProjectHostSetupProjection(repos, projects, setups, derived)
|
||||
// Why: older runtimes/profiles may hydrate empty or partial project/setup arrays
|
||||
// beside legacy repos. Keep repo-backed compatibility rows visible in that case.
|
||||
const projection = {
|
||||
projects: mergeById(derived.projects, normalized.projects),
|
||||
setups: mergeById(derived.setups, normalized.setups)
|
||||
}
|
||||
const nextCachedByProjects =
|
||||
cachedByProjects ??
|
||||
new WeakMap<Project[], WeakMap<ProjectHostSetup[], ProjectHostSetupProjection>>()
|
||||
const nextCachedBySetups =
|
||||
cachedBySetups ?? new WeakMap<ProjectHostSetup[], ProjectHostSetupProjection>()
|
||||
nextCachedBySetups.set(setups, projection)
|
||||
if (!cachedBySetups) {
|
||||
nextCachedByProjects.set(projects, nextCachedBySetups)
|
||||
}
|
||||
if (!cachedByProjects) {
|
||||
mergedProjectHostSetupProjectionCache.set(repos, nextCachedByProjects)
|
||||
}
|
||||
return projection
|
||||
}
|
||||
|
||||
function getCachedNormalizedProjectHostSetupProjection(
|
||||
repos: AppState['repos'],
|
||||
projects: Project[],
|
||||
setups: ProjectHostSetup[],
|
||||
derived: ProjectHostSetupProjection,
|
||||
normalized: ProjectHostSetupProjection
|
||||
): ProjectHostSetupProjection {
|
||||
const cachedByProjects = normalizedProjectHostSetupProjectionCache.get(repos)
|
||||
const cachedBySetups = cachedByProjects?.get(projects)
|
||||
const cachedProjection = cachedBySetups?.get(setups)
|
||||
if (cachedProjection) {
|
||||
return cachedProjection
|
||||
}
|
||||
const projection = {
|
||||
projects: mergeById(derived.projects, normalized.projects),
|
||||
setups: mergeById(derived.setups, normalized.setups)
|
||||
}
|
||||
const nextCachedByProjects =
|
||||
cachedByProjects ??
|
||||
new WeakMap<Project[], WeakMap<ProjectHostSetup[], ProjectHostSetupProjection>>()
|
||||
const nextCachedBySetups =
|
||||
cachedBySetups ?? new WeakMap<ProjectHostSetup[], ProjectHostSetupProjection>()
|
||||
nextCachedBySetups.set(setups, projection)
|
||||
if (!cachedBySetups) {
|
||||
nextCachedByProjects.set(projects, nextCachedBySetups)
|
||||
}
|
||||
if (!cachedByProjects) {
|
||||
normalizedProjectHostSetupProjectionCache.set(repos, nextCachedByProjects)
|
||||
}
|
||||
derivedProjectionByRepos.set(repos, projection)
|
||||
return projection
|
||||
}
|
||||
|
||||
// Why: this is a zustand selector compared with Object.is, so the result must be
|
||||
// reference-stable per (repos, projects, setups) input or every render re-renders.
|
||||
export function getProjectHostSetupProjectionFromState(
|
||||
state: Pick<AppState, 'repos'> & Partial<Pick<AppState, 'projects' | 'projectHostSetups'>>
|
||||
): ProjectHostSetupProjection {
|
||||
const projects = state.projects
|
||||
const projectHostSetups = state.projectHostSetups
|
||||
if (projects && projectHostSetups) {
|
||||
const cachedProjection = getCachedStateProjectHostSetupProjection(
|
||||
state.repos,
|
||||
projects,
|
||||
projectHostSetups
|
||||
)
|
||||
if (cachedProjection) {
|
||||
return cachedProjection
|
||||
}
|
||||
|
||||
const repoIds = new Set(state.repos.map((repo) => repo.id))
|
||||
const coveredRepoIds = new Set<string>()
|
||||
for (const setup of projectHostSetups) {
|
||||
const repoId = typeof setup.repoId === 'string' ? setup.repoId : ''
|
||||
if (repoIds.has(repoId)) {
|
||||
coveredRepoIds.add(repoId)
|
||||
}
|
||||
if (repoIds.has(setup.id)) {
|
||||
coveredRepoIds.add(setup.id)
|
||||
}
|
||||
}
|
||||
let projection: ProjectHostSetupProjection
|
||||
if (state.repos.length > 0 && coveredRepoIds.size < repoIds.size) {
|
||||
projection = mergeProjectHostSetupProjection(
|
||||
state.repos,
|
||||
projects as Project[],
|
||||
projectHostSetups as ProjectHostSetup[]
|
||||
)
|
||||
} else {
|
||||
const derived = getCachedProjectHostSetupProjection(state.repos)
|
||||
const normalized = normalizeHydratedProjectHostSetupProjection(
|
||||
state.repos,
|
||||
projects as Project[],
|
||||
projectHostSetups as ProjectHostSetup[],
|
||||
derived
|
||||
)
|
||||
// Why: this is a zustand selector compared with Object.is, so the merged
|
||||
// result must be reference-stable per (repos, projects, setups) input or
|
||||
// every render returns a fresh object and triggers a re-render storm.
|
||||
projection = normalized.changed
|
||||
? getCachedNormalizedProjectHostSetupProjection(
|
||||
state.repos,
|
||||
projects as Project[],
|
||||
projectHostSetups as ProjectHostSetup[],
|
||||
derived,
|
||||
normalized
|
||||
)
|
||||
: getCachedProvidedProjectHostSetupProjection(
|
||||
projects as Project[],
|
||||
projectHostSetups as ProjectHostSetup[]
|
||||
)
|
||||
}
|
||||
cacheStateProjectHostSetupProjection(state.repos, projects, projectHostSetups, projection)
|
||||
return projection
|
||||
const { repos, projects, projectHostSetups: setups } = state
|
||||
if (!projects || !setups) {
|
||||
return getDerivedProjection(repos)
|
||||
}
|
||||
return getCachedProjectHostSetupProjection(state.repos)
|
||||
let byProjects = resolvedProjectionByCatalog.get(repos)
|
||||
if (!byProjects) {
|
||||
byProjects = new WeakMap()
|
||||
resolvedProjectionByCatalog.set(repos, byProjects)
|
||||
}
|
||||
let bySetups = byProjects.get(projects)
|
||||
if (!bySetups) {
|
||||
bySetups = new WeakMap()
|
||||
byProjects.set(projects, bySetups)
|
||||
}
|
||||
const cached = bySetups.get(setups)
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
const projection = resolveProjectHostSetupProjection(
|
||||
repos,
|
||||
projects,
|
||||
setups,
|
||||
getDerivedProjection(repos)
|
||||
)
|
||||
bySetups.set(setups, projection)
|
||||
return projection
|
||||
}
|
||||
|
||||
@@ -8,10 +8,6 @@ import {
|
||||
projectHostSetupProjectionFromRepos,
|
||||
type ProjectHostSetupProjection
|
||||
} from '../../../../shared/project-host-setup-projection'
|
||||
import {
|
||||
normalizeProjectHostSetupRows,
|
||||
normalizeProjectRows
|
||||
} from '../../../../shared/project-catalog-row-normalization'
|
||||
import {
|
||||
PROJECT_HOST_SETUP_RUNTIME_CAPABILITY,
|
||||
WORKSPACE_RUN_CONTEXT_RUNTIME_CAPABILITY
|
||||
@@ -22,7 +18,7 @@ import {
|
||||
callRuntimeRpc
|
||||
} from '../../runtime/runtime-rpc-client'
|
||||
import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target'
|
||||
import { adoptFromEndpoint } from '../adopt-from-endpoint'
|
||||
import { adoptEndpointProjectCatalog } from '../../../../shared/endpoint-catalog-adoption'
|
||||
|
||||
export function getProjectSetupRuntimeTarget(
|
||||
hostId: ProjectHostSetupExistingFolderArgs['hostId']
|
||||
@@ -33,15 +29,6 @@ export function getProjectSetupRuntimeTarget(
|
||||
: { kind: 'local' }
|
||||
}
|
||||
|
||||
function normalizeProjectCatalogProjection(
|
||||
projection: ProjectHostSetupProjection
|
||||
): ProjectHostSetupProjection {
|
||||
return {
|
||||
projects: normalizeProjectRows([...projection.projects]),
|
||||
setups: normalizeProjectHostSetupRows([...projection.setups])
|
||||
}
|
||||
}
|
||||
|
||||
async function assertProjectHostSetupRuntimeCapability(target: RuntimeClientTarget): Promise<void> {
|
||||
if (target.kind !== 'environment') {
|
||||
return
|
||||
@@ -71,10 +58,11 @@ export async function fetchProjectHostSetupCompatibility(
|
||||
if (!projectsApi?.list || !projectsApi.listHostSetups) {
|
||||
throw new Error('projects_api_unavailable')
|
||||
}
|
||||
return normalizeProjectCatalogProjection({
|
||||
projects: await projectsApi.list(),
|
||||
setups: await projectsApi.listHostSetups()
|
||||
})
|
||||
return adoptEndpointProjectCatalog(
|
||||
target,
|
||||
await projectsApi.list(),
|
||||
await projectsApi.listHostSetups()
|
||||
)
|
||||
}
|
||||
await assertProjectHostSetupRuntimeCapability(target)
|
||||
const [projectResponse, setupResponse] = await Promise.all([
|
||||
@@ -85,14 +73,7 @@ export async function fetchProjectHostSetupCompatibility(
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
])
|
||||
return {
|
||||
// Why projects too: the same wire response carries them, and a remote host on another
|
||||
// Orca version can publish a row whose declared field types do not hold.
|
||||
projects: normalizeProjectRows([...projectResponse.projects]),
|
||||
setups: setupResponse.setups.map((setup) =>
|
||||
adoptFromEndpoint(target, { kind: 'projectHostSetup', row: setup })
|
||||
)
|
||||
}
|
||||
return adoptEndpointProjectCatalog(target, projectResponse.projects, setupResponse.setups)
|
||||
} catch {
|
||||
// Why: newer clients must hydrate against older runtimes that only know repo.list; derive the transitional model locally.
|
||||
return projectHostSetupProjectionFromRepos(repos)
|
||||
|
||||
@@ -5,7 +5,7 @@ import { callRuntimeRpc } from '../../runtime/runtime-rpc-client'
|
||||
import { notifyInstalledAgentSkillsChanged } from '@/hooks/installed-agent-skill-discovery'
|
||||
import type { RepoSlice } from '../repos/repo-state'
|
||||
import { getProjectSetupRuntimeTarget } from './project-host-routing'
|
||||
import { mergeUpdatedProjectCompatibilityProject } from './project-compatibility-core'
|
||||
import { mergeUpdatedProjectCompatibilityProject } from '../../../../shared/project-compatibility-merge'
|
||||
import { normalizeProjectRow } from '../../../../shared/project-catalog-row-normalization'
|
||||
|
||||
export function createProjectUpdateActions(
|
||||
|
||||
@@ -27,12 +27,11 @@ import {
|
||||
filterSetupsForPrunedRepoRows,
|
||||
filterTrustedOrcaHooksToValidRepos,
|
||||
mergeFetchedRepoCatalog,
|
||||
projectCompatibilityForReconciledRepos,
|
||||
reconcileReadoptedSshWorktreeState,
|
||||
reconcileSupersededSshRepos
|
||||
} from './repo-catalog-merge'
|
||||
import { getRuntimeTargetHostId } from '../../runtime/runtime-client-target'
|
||||
import { mergeFetchedProjectCompatibilityForHost } from '../projects/project-compatibility-host-merge'
|
||||
import { mergeFetchedHostProjectCatalog } from '../../../../shared/project-compatibility-host-merge'
|
||||
import { scheduleSafeAutoForkSync } from './safe-auto-fork-sync'
|
||||
|
||||
export function createAllHostRepoCatalogActions(
|
||||
@@ -67,11 +66,7 @@ export function createAllHostRepoCatalogActions(
|
||||
const result = mergeFetchedRepoCatalog(catalog, s.repos)
|
||||
const reconciliation = reconcileSupersededSshRepos(result.repos, s)
|
||||
const finalizedRepos = applyManualRepoOrder(reconciliation.repos, s.manualRepoOrder)
|
||||
const projectCompatibility = projectCompatibilityForReconciledRepos(
|
||||
finalizedRepos,
|
||||
catalog.projectHostSetupCompatibility
|
||||
)
|
||||
const mergedProjectCompatibility = mergeFetchedProjectCompatibilityForHost({
|
||||
const mergedProjectCompatibility = mergeFetchedHostProjectCatalog({
|
||||
previous: {
|
||||
projects: s.projects,
|
||||
projectHostSetups: filterSetupsForPrunedRepoRows(
|
||||
@@ -80,7 +75,7 @@ export function createAllHostRepoCatalogActions(
|
||||
finalizedRepos
|
||||
)
|
||||
},
|
||||
fetched: projectCompatibility,
|
||||
fetched: catalog.projectHostSetupCompatibility,
|
||||
repos: finalizedRepos,
|
||||
hostId: result.hostId
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { StateCreator } from 'zustand'
|
||||
import type { AppState } from '../types'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import { applyManualRepoOrder } from '../../../../shared/manual-repo-order'
|
||||
import { reconcileCatalogRows } from '../slices/repo-identity-reconcile'
|
||||
import { reconcileCatalogRows } from '../../../../shared/catalog-row-reconcile'
|
||||
import { retainValidFilterRepoIds } from '../slices/repo-filter-selection'
|
||||
import {
|
||||
mergeSshRepoReadoptions,
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
fetchRepoCatalogForTarget,
|
||||
filterSetupsForPrunedRepoRows,
|
||||
mergeFetchedRepoCatalog,
|
||||
projectCompatibilityForReconciledRepos,
|
||||
reconcileReadoptedSshWorktreeState,
|
||||
reconcileSupersededSshRepos
|
||||
} from './repo-catalog-merge'
|
||||
@@ -33,8 +32,8 @@ import {
|
||||
getProjectHostSetupOwnerKey,
|
||||
mergeProjectHostSetupCompatibility,
|
||||
projectCompatibilityFromRepos
|
||||
} from '../projects/project-compatibility-core'
|
||||
import { mergeFetchedProjectCompatibilityForHost } from '../projects/project-compatibility-host-merge'
|
||||
} from '../../../../shared/project-compatibility-merge'
|
||||
import { mergeFetchedHostProjectCatalog } from '../../../../shared/project-compatibility-host-merge'
|
||||
import { scheduleSafeAutoForkSync } from './safe-auto-fork-sync'
|
||||
|
||||
export function createRepoCatalogActions(
|
||||
@@ -65,7 +64,7 @@ export function createRepoCatalogActions(
|
||||
)
|
||||
// Why: mergeProjectHostSetupCompatibility always allocates; a no-op readoption
|
||||
// must not churn catalog identity. This write is all-repos, not host-scoped,
|
||||
// so it cannot go through mergeFetchedProjectCompatibilityForHost. Reconcile
|
||||
// so it cannot go through mergeFetchedHostProjectCatalog. Reconcile
|
||||
// hands the store arrays straight back when nothing moved, so writing them
|
||||
// unconditionally still leaves identity-keyed selectors untouched.
|
||||
const projects = reconcileCatalogRows(
|
||||
@@ -113,11 +112,7 @@ export function createRepoCatalogActions(
|
||||
const prunedRepos = applyManualRepoOrder(reconciliation.repos, s.manualRepoOrder)
|
||||
const validRepoIds = new Set(prunedRepos.map((repo) => repo.id))
|
||||
const validRepoHostIdentities = new Set(prunedRepos.map(getRepoHostIdentity))
|
||||
const projectCompatibility = projectCompatibilityForReconciledRepos(
|
||||
prunedRepos,
|
||||
catalog.projectHostSetupCompatibility
|
||||
)
|
||||
const mergedProjectCompatibility = mergeFetchedProjectCompatibilityForHost({
|
||||
const mergedProjectCompatibility = mergeFetchedHostProjectCatalog({
|
||||
previous: {
|
||||
projects: s.projects,
|
||||
projectHostSetups: filterSetupsForPrunedRepoRows(
|
||||
@@ -126,7 +121,7 @@ export function createRepoCatalogActions(
|
||||
prunedRepos
|
||||
)
|
||||
},
|
||||
fetched: projectCompatibility,
|
||||
fetched: catalog.projectHostSetupCompatibility,
|
||||
repos: prunedRepos,
|
||||
hostId: result.hostId
|
||||
})
|
||||
|
||||
@@ -4,8 +4,8 @@ import { reconcileFetchedRepos } from '../slices/repo-identity-reconcile'
|
||||
import { getRepoHostIdentity } from '../slices/repo-host-identity'
|
||||
import { getRepoExecutionHostId } from '../../../../shared/execution-host'
|
||||
import type { RepoSlice } from './repo-state'
|
||||
import { mergeFetchedProjectCompatibilityForHost } from '../projects/project-compatibility-host-merge'
|
||||
import { projectCompatibilityFromRepos } from '../projects/project-compatibility-core'
|
||||
import { mergeFetchedProjectCompatibilityForHost } from '../../../../shared/project-compatibility-host-merge'
|
||||
import { projectCompatibilityFromRepos } from '../../../../shared/project-compatibility-merge'
|
||||
import { mergeByIdentity } from '../catalog-identity'
|
||||
|
||||
export function mergeFetchedReposForHost(
|
||||
|
||||
@@ -9,15 +9,10 @@ import { reconcileReadoptedSshWorktreesByRepo } from '../slices/readopted-ssh-wo
|
||||
import { callRuntimeRpc } from '../../runtime/runtime-rpc-client'
|
||||
import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target'
|
||||
import { getRepoExecutionHostId } from '../../../../shared/execution-host'
|
||||
import type { RepoSlice } from './repo-state'
|
||||
import { adoptFromEndpoint } from '../adopt-from-endpoint'
|
||||
import { getRuntimeTargetHostId } from '../../runtime/runtime-client-target'
|
||||
import { fetchProjectHostSetupCompatibility } from '../projects/project-host-routing'
|
||||
import { mergeFetchedReposForHost } from './repo-catalog-identity'
|
||||
import {
|
||||
mergeProjectHostSetupCompatibility,
|
||||
projectCompatibilityFromRepos
|
||||
} from '../projects/project-compatibility-core'
|
||||
|
||||
export type FetchedRepoCatalog = {
|
||||
repos: readonly Repo[]
|
||||
@@ -120,13 +115,6 @@ export function reconcileReadoptedSshWorktreeState(
|
||||
}
|
||||
}
|
||||
|
||||
export function projectCompatibilityForReconciledRepos(
|
||||
repos: readonly Repo[],
|
||||
fetched: ProjectHostSetupProjection
|
||||
): Pick<RepoSlice, 'projects' | 'projectHostSetups'> {
|
||||
return mergeProjectHostSetupCompatibility(projectCompatibilityFromRepos(repos), fetched)
|
||||
}
|
||||
|
||||
export function filterTrustedOrcaHooksToValidRepos(
|
||||
trust: AppState['trustedOrcaHooks'],
|
||||
validRepoIds: Set<string>
|
||||
|
||||
@@ -15,12 +15,11 @@ import {
|
||||
fetchRepoCatalogForTarget,
|
||||
filterSetupsForPrunedRepoRows,
|
||||
mergeFetchedRepoCatalog,
|
||||
projectCompatibilityForReconciledRepos,
|
||||
reconcileReadoptedSshWorktreeState,
|
||||
reconcileSupersededSshRepos
|
||||
} from './repo-catalog-merge'
|
||||
import { getRuntimeTargetHostId } from '../../runtime/runtime-client-target'
|
||||
import { mergeFetchedProjectCompatibilityForHost } from '../projects/project-compatibility-host-merge'
|
||||
import { mergeFetchedHostProjectCatalog } from '../../../../shared/project-compatibility-host-merge'
|
||||
import { scheduleSafeAutoForkSync } from './safe-auto-fork-sync'
|
||||
|
||||
export function createRuntimeRepoCatalogActions(
|
||||
@@ -56,11 +55,7 @@ export function createRuntimeRepoCatalogActions(
|
||||
const finalizedRepos = applyManualRepoOrder(reconciliation.repos, s.manualRepoOrder)
|
||||
const validRepoIds = new Set(finalizedRepos.map((repo) => repo.id))
|
||||
const validRepoHostIdentities = new Set(finalizedRepos.map(getRepoHostIdentity))
|
||||
const projectCompatibility = projectCompatibilityForReconciledRepos(
|
||||
finalizedRepos,
|
||||
catalog.projectHostSetupCompatibility
|
||||
)
|
||||
const mergedProjectCompatibility = mergeFetchedProjectCompatibilityForHost({
|
||||
const mergedProjectCompatibility = mergeFetchedHostProjectCatalog({
|
||||
previous: {
|
||||
projects: s.projects,
|
||||
projectHostSetups: filterSetupsForPrunedRepoRows(
|
||||
@@ -69,7 +64,7 @@ export function createRuntimeRepoCatalogActions(
|
||||
finalizedRepos
|
||||
)
|
||||
},
|
||||
fetched: projectCompatibility,
|
||||
fetched: catalog.projectHostSetupCompatibility,
|
||||
repos: finalizedRepos,
|
||||
hostId: result.hostId
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import { structuralValuesEqual } from '../../../../shared/structural-value-equality'
|
||||
import { getRepoHostIdentity } from './repo-host-identity'
|
||||
import { reconcileCatalogRows } from '../../../../shared/catalog-row-reconcile'
|
||||
|
||||
// Why: after a drag-reorder we optimistically set `repos`, persist, and main
|
||||
// broadcasts `repos:changed`. The renderer's own echo handler refetches, which
|
||||
@@ -13,32 +14,6 @@ import { getRepoHostIdentity } from './repo-host-identity'
|
||||
// gitRemoteIdentity, repoIcon, path arrays) that structured-clone and main's hydrateRepo rebuild
|
||||
// every fetch, so a reference compare would report every repo as changed.
|
||||
|
||||
/**
|
||||
* Reuses equal rows from `previous` — and the whole array when nothing moved — so a refetch that
|
||||
* changed nothing leaves identity-keyed memos and store subscribers untouched. `getIdentity` must
|
||||
* be the key the producing merge already dedups by, so it is unique within `next`.
|
||||
*/
|
||||
export function reconcileCatalogRows<T>(
|
||||
previous: readonly T[],
|
||||
next: readonly T[],
|
||||
getIdentity: (row: T) => string
|
||||
): readonly T[] {
|
||||
const previousByIdentity = new Map(previous.map((row) => [getIdentity(row), row]))
|
||||
let identical = next.length === previous.length
|
||||
const reconciled = next.map((row, index) => {
|
||||
const existing = previousByIdentity.get(getIdentity(row))
|
||||
if (existing !== undefined && structuralValuesEqual(existing, row)) {
|
||||
if (existing !== previous[index]) {
|
||||
identical = false
|
||||
}
|
||||
return existing
|
||||
}
|
||||
identical = false
|
||||
return row
|
||||
})
|
||||
return identical ? previous : reconciled
|
||||
}
|
||||
|
||||
export function reconcileFetchedRepos(
|
||||
previous: readonly Repo[],
|
||||
next: readonly Repo[]
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
dismissRuntimeDisconnectedToast,
|
||||
showRuntimeDisconnectedToast
|
||||
} from './runtime-environment-disconnect-toast'
|
||||
import { reconcileCatalogRows } from './repo-identity-reconcile'
|
||||
import { reconcileCatalogRows } from '../../../../shared/catalog-row-reconcile'
|
||||
import { createRuntimeStatusHydration } from './runtime-status-hydration'
|
||||
import { refreshRuntimeEnvironmentStatus } from './runtime-status-refresh'
|
||||
import * as runtimeStatusConnectionGeneration from './runtime-status-connection-generation'
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { structuralValuesEqual } from './structural-value-equality'
|
||||
|
||||
/**
|
||||
* Reuses equal rows from `previous` — and the whole array when nothing moved — so a refetch that
|
||||
* changed nothing leaves identity-keyed memos and store subscribers untouched. `getIdentity` must
|
||||
* be the key the producing merge already dedups by, so it is unique within `next`.
|
||||
*/
|
||||
export function reconcileCatalogRows<T>(
|
||||
previous: readonly T[],
|
||||
next: readonly T[],
|
||||
getIdentity: (row: T) => string
|
||||
): readonly T[] {
|
||||
const previousByIdentity = new Map(previous.map((row) => [getIdentity(row), row]))
|
||||
let identical = next.length === previous.length
|
||||
const reconciled = next.map((row, index) => {
|
||||
const existing = previousByIdentity.get(getIdentity(row))
|
||||
if (existing !== undefined && structuralValuesEqual(existing, row)) {
|
||||
if (existing !== previous[index]) {
|
||||
identical = false
|
||||
}
|
||||
return existing
|
||||
}
|
||||
identical = false
|
||||
return row
|
||||
})
|
||||
return identical ? previous : reconciled
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// The host stamp rows take entering a client's all-host catalog; shared so the phone matches the desktop.
|
||||
import {
|
||||
getRepoExecutionHostId,
|
||||
LOCAL_EXECUTION_HOST_ID,
|
||||
toRuntimeExecutionHostId
|
||||
} from './execution-host'
|
||||
import {
|
||||
normalizeProjectHostSetupRow,
|
||||
normalizeProjectHostSetupRows,
|
||||
normalizeProjectRows
|
||||
} from './project-catalog-row-normalization'
|
||||
import type { ProjectHostSetupProjection } from './project-host-setup-projection'
|
||||
import type { Project, ProjectHostSetup } from './project-types'
|
||||
import type { Repo } from './repo-types'
|
||||
import type { RuntimeClientTarget } from './runtime-client-target'
|
||||
|
||||
export function adoptEndpointRepo(target: RuntimeClientTarget, repo: Repo): Repo {
|
||||
if (target.kind === 'environment') {
|
||||
// `connectionId` survives: on a server's row it names that server's nested SSH target.
|
||||
return { ...repo, executionHostId: toRuntimeExecutionHostId(target.environmentId) }
|
||||
}
|
||||
if (repo.connectionId) {
|
||||
return { ...repo, executionHostId: getRepoExecutionHostId(repo) }
|
||||
}
|
||||
return repo.executionHostId ? repo : { ...repo, executionHostId: LOCAL_EXECUTION_HOST_ID }
|
||||
}
|
||||
|
||||
export function adoptEndpointProjectHostSetup(
|
||||
target: RuntimeClientTarget,
|
||||
setup: ProjectHostSetup
|
||||
): ProjectHostSetup {
|
||||
// Why here: every setup row crossing IPC or RPC into a client passes through this
|
||||
// adoption step, so it is where the declared field types stop being aspirational.
|
||||
const adopted = normalizeProjectHostSetupRow(setup)
|
||||
if (target.kind !== 'environment') {
|
||||
return adopted
|
||||
}
|
||||
const hostId = toRuntimeExecutionHostId(target.environmentId)
|
||||
const executionHostId = adopted.executionHostId ?? adopted.hostId
|
||||
return {
|
||||
...adopted,
|
||||
hostId,
|
||||
executionHostId: executionHostId === LOCAL_EXECUTION_HOST_ID ? hostId : executionHostId,
|
||||
runtimeOwnerEnvironmentId: target.environmentId,
|
||||
// Why: paired clients route through the HUB and must not treat its private SSH target as client-local configuration.
|
||||
connectionId: null
|
||||
}
|
||||
}
|
||||
|
||||
/** An endpoint's `project.list` and `projectHostSetup.list` answers as a client's projection. */
|
||||
export function adoptEndpointProjectCatalog(
|
||||
target: RuntimeClientTarget,
|
||||
projects: readonly Project[],
|
||||
setups: readonly ProjectHostSetup[]
|
||||
): ProjectHostSetupProjection {
|
||||
if (target.kind === 'local') {
|
||||
return {
|
||||
projects: normalizeProjectRows([...projects]),
|
||||
setups: normalizeProjectHostSetupRows([...setups])
|
||||
}
|
||||
}
|
||||
return {
|
||||
// Why projects too: the same wire response carries them, and a remote host on another
|
||||
// Orca version can publish a row whose declared field types do not hold.
|
||||
projects: normalizeProjectRows([...projects]),
|
||||
setups: setups.map((setup) => adoptEndpointProjectHostSetup(target, setup))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { ProjectHostSetupProjection } from './project-host-setup-projection'
|
||||
import type { ProjectSectionProject, ProjectSectionSetup } from './project-section-grouping'
|
||||
import type { RepoIcon } from './repo-icon'
|
||||
import type { Repo } from './repo-types'
|
||||
import type { ProjectOrderBy } from './ui-chrome-types'
|
||||
|
||||
// Why the desktop answers it: its sidebar's own grouping inputs, so the phone's Project sections match.
|
||||
export const MOBILE_PROJECT_SECTIONS_METHOD = 'mobileRelay.projectSections'
|
||||
|
||||
/** A sidebar repo as Project sections read it; never its hooks, credentials or AI settings. */
|
||||
export type MobileProjectSectionRepo = Pick<Repo, 'id' | 'displayName' | 'path' | 'badgeColor'> &
|
||||
Partial<Pick<Repo, 'addedAt' | 'connectionId' | 'executionHostId'>> & { repoIcon?: RepoIcon }
|
||||
|
||||
export type MobileProjectSectionCatalog = {
|
||||
/** Every repo the sidebar holds, across hosts, in its header order. */
|
||||
repos: MobileProjectSectionRepo[]
|
||||
projects: ProjectSectionProject[]
|
||||
setups: ProjectSectionSetup[]
|
||||
projectOrderBy: ProjectOrderBy
|
||||
}
|
||||
|
||||
/** Null until a desktop window has published its sidebar catalog. */
|
||||
export type MobileProjectSectionsResult = { catalog: MobileProjectSectionCatalog | null }
|
||||
|
||||
function sectionRepo(repo: Repo): MobileProjectSectionRepo {
|
||||
return {
|
||||
id: repo.id,
|
||||
displayName: repo.displayName,
|
||||
path: repo.path,
|
||||
badgeColor: repo.badgeColor,
|
||||
addedAt: repo.addedAt,
|
||||
...(repo.connectionId !== undefined ? { connectionId: repo.connectionId } : {}),
|
||||
...(repo.executionHostId ? { executionHostId: repo.executionHostId } : {}),
|
||||
// Why not uploaded icons: a data URL can be hundreds of KB per repo on every read.
|
||||
...(repo.repoIcon && !(repo.repoIcon.type === 'image' && repo.repoIcon.src.startsWith('data:'))
|
||||
? { repoIcon: repo.repoIcon }
|
||||
: {})
|
||||
}
|
||||
}
|
||||
|
||||
/** The sidebar's repos, in its order, and the projection its project-header selector produced. */
|
||||
export function buildMobileProjectSectionCatalog(
|
||||
repos: readonly Repo[],
|
||||
projection: ProjectHostSetupProjection,
|
||||
projectOrderBy: ProjectOrderBy
|
||||
): MobileProjectSectionCatalog {
|
||||
return {
|
||||
repos: repos.map(sectionRepo),
|
||||
projects: projection.projects.map(({ id, displayName }) => ({ id, displayName })),
|
||||
setups: projection.setups.map((setup) => ({
|
||||
id: setup.id,
|
||||
projectId: setup.projectId,
|
||||
hostId: setup.hostId,
|
||||
repoId: setup.repoId,
|
||||
path: setup.path,
|
||||
displayName: setup.displayName,
|
||||
setupMethod: setup.setupMethod,
|
||||
...(setup.kind ? { kind: setup.kind } : {}),
|
||||
...(setup.connectionId !== undefined ? { connectionId: setup.connectionId } : {}),
|
||||
...(setup.executionHostId ? { executionHostId: setup.executionHostId } : {})
|
||||
})),
|
||||
projectOrderBy
|
||||
}
|
||||
}
|
||||
+32
-9
@@ -1,21 +1,24 @@
|
||||
import type { Project, ProjectHostSetup } from '../../../../shared/project-types'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import { reconcileCatalogRows } from '../slices/repo-identity-reconcile'
|
||||
import type { Project, ProjectHostSetup } from './project-types'
|
||||
import type { Repo } from './repo-types'
|
||||
import { reconcileCatalogRows } from './catalog-row-reconcile'
|
||||
import {
|
||||
getRepoExecutionHostId,
|
||||
LOCAL_EXECUTION_HOST_ID,
|
||||
parseExecutionHostId
|
||||
} from '../../../../shared/execution-host'
|
||||
import type { RepoSlice } from '../repos/repo-state'
|
||||
} from './execution-host'
|
||||
import {
|
||||
type ProjectCatalogRows,
|
||||
getProjectHostSetupOwnerKey,
|
||||
getReposById,
|
||||
getSourceRepoIdsOutsideHost,
|
||||
mergePreviousProjectMetadata,
|
||||
mergeProjectCompatibilityProjects,
|
||||
mergeProjectHostSetupCompatibility,
|
||||
mergeProjectHostSetupsByOwner,
|
||||
projectCompatibilityFromRepos,
|
||||
projectWithCurrentSourceRepoIds
|
||||
} from './project-compatibility-core'
|
||||
} from './project-compatibility-merge'
|
||||
import type { ProjectHostSetupProjection } from './project-host-setup-projection'
|
||||
|
||||
export function getProjectHostIds(
|
||||
project: Project,
|
||||
@@ -130,11 +133,11 @@ export function mergeFetchedProjectCompatibilityForHost({
|
||||
repos,
|
||||
hostId
|
||||
}: {
|
||||
previous: Pick<RepoSlice, 'projects' | 'projectHostSetups'>
|
||||
fetched: Pick<RepoSlice, 'projects' | 'projectHostSetups'>
|
||||
previous: ProjectCatalogRows
|
||||
fetched: ProjectCatalogRows
|
||||
repos: readonly Repo[]
|
||||
hostId: string
|
||||
}): Pick<RepoSlice, 'projects' | 'projectHostSetups'> {
|
||||
}): ProjectCatalogRows {
|
||||
const setupBelongsToFetchedCatalog = (setup: ProjectHostSetup): boolean => {
|
||||
if (hostId !== LOCAL_EXECUTION_HOST_ID) {
|
||||
return setup.hostId === hostId
|
||||
@@ -224,3 +227,23 @@ export function mergeFetchedProjectCompatibilityForHost({
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** One host's fetched catalog folded into the all-host project rows, as each host refresh does. */
|
||||
export function mergeFetchedHostProjectCatalog({
|
||||
previous,
|
||||
fetched,
|
||||
repos,
|
||||
hostId
|
||||
}: {
|
||||
previous: ProjectCatalogRows
|
||||
fetched: ProjectHostSetupProjection
|
||||
repos: readonly Repo[]
|
||||
hostId: string
|
||||
}): ProjectCatalogRows {
|
||||
return mergeFetchedProjectCompatibilityForHost({
|
||||
previous,
|
||||
fetched: mergeProjectHostSetupCompatibility(projectCompatibilityFromRepos(repos), fetched),
|
||||
repos,
|
||||
hostId
|
||||
})
|
||||
}
|
||||
+15
-12
@@ -1,17 +1,20 @@
|
||||
import type { Project, ProjectHostSetup } from '../../../../shared/project-types'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import type { Project, ProjectHostSetup, ProjectUpdateArgs } from './project-types'
|
||||
import type { Repo } from './repo-types'
|
||||
import {
|
||||
mergeCatalogCreatedAt,
|
||||
mergeCatalogUpdatedAt,
|
||||
projectHostSetupProjectionFromRepos
|
||||
} from '../../../../shared/project-host-setup-projection'
|
||||
import type { ProjectHostSetupProjection } from '../../../../shared/project-host-setup-projection'
|
||||
import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
|
||||
import type { ProjectUpdate, RepoSlice } from '../repos/repo-state'
|
||||
} from './project-host-setup-projection'
|
||||
import type { ProjectHostSetupProjection } from './project-host-setup-projection'
|
||||
import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from './execution-host'
|
||||
|
||||
export function projectCompatibilityFromRepos(
|
||||
repos: readonly Repo[]
|
||||
): Pick<RepoSlice, 'projects' | 'projectHostSetups'> {
|
||||
/** A catalog's project rows and the host setups that place them. */
|
||||
export type ProjectCatalogRows = {
|
||||
projects: readonly Project[]
|
||||
projectHostSetups: readonly ProjectHostSetup[]
|
||||
}
|
||||
|
||||
export function projectCompatibilityFromRepos(repos: readonly Repo[]): ProjectCatalogRows {
|
||||
const projection = projectHostSetupProjectionFromRepos(repos)
|
||||
return {
|
||||
projects: projection.projects,
|
||||
@@ -63,7 +66,7 @@ export function mergeProjectCompatibilityProjects(
|
||||
export function mergeUpdatedProjectCompatibilityProject(
|
||||
base: Project,
|
||||
updated: Project,
|
||||
updates: ProjectUpdate
|
||||
updates: ProjectUpdateArgs['updates']
|
||||
): Project {
|
||||
const project = mergeProjectCompatibilityProject(base, updated)
|
||||
if ('localWindowsRuntimePreference' in updates) {
|
||||
@@ -189,9 +192,9 @@ export function mergePreviousProjectMetadata(
|
||||
}
|
||||
|
||||
export function mergeProjectHostSetupCompatibility(
|
||||
derived: Pick<RepoSlice, 'projects' | 'projectHostSetups'>,
|
||||
derived: ProjectCatalogRows,
|
||||
fetched: ProjectHostSetupProjection
|
||||
): Pick<RepoSlice, 'projects' | 'projectHostSetups'> {
|
||||
): ProjectCatalogRows {
|
||||
const fetchedRepoSetupKeys = new Set(fetched.setups.map(getRepoDerivedSetupKey))
|
||||
const derivedSetups = derived.projectHostSetups.filter(
|
||||
(setup) => !fetchedRepoSetupKeys.has(getRepoDerivedSetupKey(setup))
|
||||
@@ -0,0 +1,120 @@
|
||||
import type { Project, ProjectHostSetup } from './project-types'
|
||||
import type { Repo } from './repo-types'
|
||||
import {
|
||||
getProjectIdentityKey,
|
||||
projectHostSetupProjectionFromRepos,
|
||||
type ProjectHostSetupProjection
|
||||
} from './project-host-setup-projection'
|
||||
|
||||
type NormalizedProjectHostSetupProjection = ProjectHostSetupProjection & {
|
||||
changed: boolean
|
||||
}
|
||||
|
||||
function normalizeHydratedProjectHostSetupProjection(
|
||||
repos: readonly Repo[],
|
||||
projects: readonly Project[],
|
||||
setups: readonly ProjectHostSetup[],
|
||||
derived: ProjectHostSetupProjection
|
||||
): NormalizedProjectHostSetupProjection {
|
||||
const repoById = new Map(repos.map((repo) => [repo.id, repo]))
|
||||
const derivedProjectIds = new Set(derived.projects.map((project) => project.id))
|
||||
const projectIdByHydratedProjectId = new Map<string, string>()
|
||||
let changed = false
|
||||
const normalizedSetups = setups.map((setup) => {
|
||||
const repo = repoById.get(setup.repoId) ?? repoById.get(setup.id)
|
||||
if (!repo) {
|
||||
return setup
|
||||
}
|
||||
const projectId = getProjectIdentityKey(repo)
|
||||
if (projectId === setup.projectId || projectId === `repo:${repo.id}`) {
|
||||
return setup
|
||||
}
|
||||
changed = true
|
||||
projectIdByHydratedProjectId.set(setup.projectId, projectId)
|
||||
return { ...setup, projectId }
|
||||
})
|
||||
const normalizedProjects = projects.flatMap((project) => {
|
||||
const projectId = projectIdByHydratedProjectId.get(project.id)
|
||||
if (!projectId || projectId === project.id) {
|
||||
return [project]
|
||||
}
|
||||
// Why: runtime-hosted copies of the same Git repo may hydrate path-scoped
|
||||
// project ids. If the repo-derived project already exists, keep that bucket
|
||||
// authoritative so VM copies group under the user's single project.
|
||||
if (derivedProjectIds.has(projectId)) {
|
||||
changed = true
|
||||
return []
|
||||
}
|
||||
changed = true
|
||||
return [{ ...project, id: projectId }]
|
||||
})
|
||||
return { projects: normalizedProjects, setups: normalizedSetups, changed }
|
||||
}
|
||||
|
||||
/** `overlay` rows replace `base` rows of the same id; new ids append in overlay order. */
|
||||
function mergeProjectionRowsById<T extends { id: string }>(
|
||||
base: readonly T[],
|
||||
overlay: readonly T[]
|
||||
): T[] {
|
||||
const merged = [...base]
|
||||
const indexById = new Map(merged.map((entry, index) => [entry.id, index]))
|
||||
for (const entry of overlay) {
|
||||
const index = indexById.get(entry.id)
|
||||
if (index === undefined) {
|
||||
indexById.set(entry.id, merged.length)
|
||||
merged.push(entry)
|
||||
} else {
|
||||
merged[index] = entry
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
/** Whether some repo has no host setup naming it, by setup repo id or setup id. */
|
||||
function hasReposWithoutProjectHostSetup(
|
||||
repos: readonly Pick<Repo, 'id'>[],
|
||||
setups: readonly ProjectHostSetup[]
|
||||
): boolean {
|
||||
const repoIds = new Set(repos.map((repo) => repo.id))
|
||||
const coveredRepoIds = new Set<string>()
|
||||
for (const setup of setups) {
|
||||
const repoId = typeof setup.repoId === 'string' ? setup.repoId : ''
|
||||
if (repoIds.has(repoId)) {
|
||||
coveredRepoIds.add(repoId)
|
||||
}
|
||||
if (repoIds.has(setup.id)) {
|
||||
coveredRepoIds.add(setup.id)
|
||||
}
|
||||
}
|
||||
return repos.length > 0 && coveredRepoIds.size < repoIds.size
|
||||
}
|
||||
|
||||
/** The repo-derived rows overlaid with the hydrated ones, so a repo without a setup still groups. */
|
||||
function mergeDerivedProjectHostSetupProjection(
|
||||
derived: ProjectHostSetupProjection,
|
||||
normalized: ProjectHostSetupProjection
|
||||
): ProjectHostSetupProjection {
|
||||
return {
|
||||
projects: mergeProjectionRowsById(derived.projects, normalized.projects),
|
||||
setups: mergeProjectionRowsById(derived.setups, normalized.setups)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The projects and setups a catalog groups by: the hydrated rows, normalized onto the repos'
|
||||
* identities, and backed by the repo-derived rows when any repo has no setup. The renderer's
|
||||
* selector is a memo over this.
|
||||
*/
|
||||
export function resolveProjectHostSetupProjection(
|
||||
repos: readonly Repo[],
|
||||
projects: readonly Project[],
|
||||
setups: readonly ProjectHostSetup[],
|
||||
derived: ProjectHostSetupProjection = projectHostSetupProjectionFromRepos(repos)
|
||||
): ProjectHostSetupProjection {
|
||||
const normalized = normalizeHydratedProjectHostSetupProjection(repos, projects, setups, derived)
|
||||
// Why: older runtimes/profiles may hydrate empty or partial project/setup arrays
|
||||
// beside legacy repos. Keep repo-backed compatibility rows visible in that case.
|
||||
return hasReposWithoutProjectHostSetup(repos, setups) || normalized.changed
|
||||
? mergeDerivedProjectHostSetupProjection(derived, normalized)
|
||||
: { projects, setups }
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
// The desktop sidebar's Project sections; the paired phone calls these same functions.
|
||||
import type { ExecutionHostId } from './execution-host'
|
||||
import { orderProjectSections, type ActivityItem } from './project-section-order'
|
||||
import type { ProjectOrderBy } from './ui-chrome-types'
|
||||
import { toSshExecutionHostId } from './execution-host'
|
||||
import type { Project, ProjectHostSetup } from './project-types'
|
||||
import type { Repo } from './repo-types'
|
||||
import { parseWslUncPath } from './wsl-paths'
|
||||
import { isWindowsAbsolutePathLike, normalizeRuntimePathForComparison } from './cross-platform-path'
|
||||
|
||||
/** The project fields grouping reads. */
|
||||
export type ProjectSectionProject = Pick<Project, 'id' | 'displayName'>
|
||||
|
||||
/** The host-setup fields grouping and host labels read. */
|
||||
export type ProjectSectionSetup = Pick<
|
||||
ProjectHostSetup,
|
||||
'id' | 'projectId' | 'hostId' | 'repoId' | 'path' | 'displayName' | 'setupMethod'
|
||||
> &
|
||||
Partial<Pick<ProjectHostSetup, 'kind' | 'connectionId' | 'executionHostId'>>
|
||||
|
||||
export type ProjectGroupingModel = {
|
||||
projects: readonly Project[]
|
||||
projectHostSetups: readonly ProjectHostSetup[]
|
||||
}
|
||||
|
||||
/** What grouping reads of a model; a paired phone receives only this much. */
|
||||
export type ProjectSectionModel = {
|
||||
projects: readonly ProjectSectionProject[]
|
||||
projectHostSetups: readonly ProjectSectionSetup[]
|
||||
}
|
||||
|
||||
export type ProjectGroupingIndex = {
|
||||
projectById: Map<string, ProjectSectionProject>
|
||||
setupByRepoId: Map<string, ProjectSectionSetup>
|
||||
surfaceKeysRequiringSetupGroups: Set<string>
|
||||
}
|
||||
|
||||
/** The repo fields section grouping, labelling and ordering read. */
|
||||
export type ProjectSectionRepo = {
|
||||
id: string
|
||||
displayName: string
|
||||
path: string
|
||||
addedAt?: number
|
||||
connectionId?: string | null
|
||||
executionHostId?: ExecutionHostId | null
|
||||
}
|
||||
|
||||
/** One project section: its rows, the repo that anchors its header, and every repo it holds. */
|
||||
export type ProjectSectionEntry<Item, R extends ProjectSectionRepo = Repo> = {
|
||||
label: string
|
||||
items: Item[]
|
||||
repo?: R
|
||||
repoIds: Set<string>
|
||||
}
|
||||
|
||||
const projectGroupingIndexCache = new WeakMap<ProjectSectionModel, ProjectGroupingIndex | null>()
|
||||
|
||||
// Why: provisioned and folder setups are not independent Git checkouts.
|
||||
function isDistinctUserCheckout(setup: ProjectSectionSetup): boolean {
|
||||
return setup.setupMethod !== 'provisioned' && setup.kind !== 'folder'
|
||||
}
|
||||
|
||||
// Why: execution target and filesystem namespace independently identify a surface.
|
||||
function getProjectSetupSurfaceKey(setup: ProjectSectionSetup): string {
|
||||
return `${setup.projectId}::${setup.hostId}::${getExecutionSurface(setup)}::${getPathSurface(setup)}`
|
||||
}
|
||||
|
||||
function getExecutionSurface(setup: ProjectSectionSetup): string {
|
||||
const connectionId = setup.connectionId?.trim()
|
||||
if (connectionId) {
|
||||
return toSshExecutionHostId(connectionId)
|
||||
}
|
||||
return setup.executionHostId?.trim() || setup.hostId
|
||||
}
|
||||
|
||||
// Why: projection twins differ by row identity, not checkout directory.
|
||||
function getCheckoutIdentity(setup: ProjectSectionSetup): string {
|
||||
return normalizeRuntimePathForComparison(setup.path.trim()) || setup.repoId || setup.id
|
||||
}
|
||||
|
||||
function getPathSurface(setup: ProjectSectionSetup): string {
|
||||
const wslPath = parseWslUncPath(setup.path)
|
||||
if (wslPath) {
|
||||
return `wsl:${wslPath.distro.toLowerCase()}`
|
||||
}
|
||||
if (isWindowsAbsolutePathLike(setup.path)) {
|
||||
return 'windows-host'
|
||||
}
|
||||
return 'default'
|
||||
}
|
||||
|
||||
export function buildProjectGroupingIndex(
|
||||
model?: ProjectSectionModel
|
||||
): ProjectGroupingIndex | null {
|
||||
if (!model) {
|
||||
return null
|
||||
}
|
||||
const cached = projectGroupingIndexCache.get(model)
|
||||
if (cached !== undefined) {
|
||||
return cached
|
||||
}
|
||||
const projects = model.projects ?? []
|
||||
const projectHostSetups = model.projectHostSetups ?? []
|
||||
if (projects.length === 0 || projectHostSetups.length === 0) {
|
||||
projectGroupingIndexCache.set(model, null)
|
||||
return null
|
||||
}
|
||||
const checkoutsByProjectSurface = new Map<string, Set<string>>()
|
||||
for (const setup of projectHostSetups) {
|
||||
if (!isDistinctUserCheckout(setup)) {
|
||||
continue
|
||||
}
|
||||
const key = getProjectSetupSurfaceKey(setup)
|
||||
const existing = checkoutsByProjectSurface.get(key)
|
||||
if (existing) {
|
||||
existing.add(getCheckoutIdentity(setup))
|
||||
} else {
|
||||
checkoutsByProjectSurface.set(key, new Set([getCheckoutIdentity(setup)]))
|
||||
}
|
||||
}
|
||||
const surfaceKeysRequiringSetupGroups = new Set<string>()
|
||||
for (const [surfaceKey, checkouts] of checkoutsByProjectSurface) {
|
||||
if (checkouts.size > 1) {
|
||||
surfaceKeysRequiringSetupGroups.add(surfaceKey)
|
||||
}
|
||||
}
|
||||
const index = {
|
||||
projectById: new Map(projects.map((project) => [project.id, project])),
|
||||
setupByRepoId: new Map(projectHostSetups.map((setup) => [setup.repoId, setup])),
|
||||
surfaceKeysRequiringSetupGroups
|
||||
}
|
||||
projectGroupingIndexCache.set(model, index)
|
||||
return index
|
||||
}
|
||||
|
||||
// Why: Settings splits projects by this same rule so it matches the sidebar headers.
|
||||
export function isCheckoutScopedProjectSetup(
|
||||
setup: ProjectSectionSetup,
|
||||
projectIndex: ProjectGroupingIndex
|
||||
): boolean {
|
||||
return (
|
||||
projectIndex.surfaceKeysRequiringSetupGroups.has(getProjectSetupSurfaceKey(setup)) &&
|
||||
isDistinctUserCheckout(setup)
|
||||
)
|
||||
}
|
||||
|
||||
export type ProjectHeaderRevealTarget<R extends Pick<ProjectSectionRepo, 'displayName'> = Repo> = {
|
||||
key: string
|
||||
label: string
|
||||
repo?: R
|
||||
projectId?: string
|
||||
}
|
||||
|
||||
export function getProjectGroupingForRepo<R extends Pick<ProjectSectionRepo, 'displayName'>>(
|
||||
repoId: string,
|
||||
repoMap: ReadonlyMap<string, R>,
|
||||
projectIndex: ProjectGroupingIndex | null
|
||||
): ProjectHeaderRevealTarget<R> {
|
||||
const repo = repoMap.get(repoId)
|
||||
const setup = projectIndex?.setupByRepoId.get(repoId)
|
||||
const project = setup ? projectIndex?.projectById.get(setup.projectId) : undefined
|
||||
if (!setup || !project) {
|
||||
return {
|
||||
key: `repo:${repoId}`,
|
||||
label: repo?.displayName ?? 'Unknown',
|
||||
repo
|
||||
}
|
||||
}
|
||||
if (projectIndex && isCheckoutScopedProjectSetup(setup, projectIndex)) {
|
||||
// Why: only the ambiguous surface needs checkout-specific headers.
|
||||
return {
|
||||
key: `project:${project.id}::setup:${repoId}`,
|
||||
label: repo?.displayName ?? setup.displayName,
|
||||
repo,
|
||||
projectId: project.id
|
||||
}
|
||||
}
|
||||
// Why: provisioned runtime copies and non-ambiguous checkouts follow project
|
||||
// identity rather than path-scoped setup identity, so they stay in one project.
|
||||
return {
|
||||
key: `project:${project.id}`,
|
||||
label: project.displayName,
|
||||
repo,
|
||||
projectId: project.id
|
||||
}
|
||||
}
|
||||
|
||||
export function getProjectHeaderRevealTarget(
|
||||
repoId: string,
|
||||
repoMap: Map<string, Repo>,
|
||||
projectGrouping?: ProjectGroupingModel
|
||||
): ProjectHeaderRevealTarget {
|
||||
return getProjectGroupingForRepo(repoId, repoMap, buildProjectGroupingIndex(projectGrouping))
|
||||
}
|
||||
|
||||
/** Files `item` under its repo's project section, opening the section on first sight. */
|
||||
function addItemToProjectSection<Item, R extends ProjectSectionRepo>(
|
||||
sections: Map<string, ProjectSectionEntry<Item, R>>,
|
||||
item: Item,
|
||||
repoId: string,
|
||||
repoMap: ReadonlyMap<string, R>,
|
||||
projectIndex: ProjectGroupingIndex | null
|
||||
): void {
|
||||
const grouping = getProjectGroupingForRepo(repoId, repoMap, projectIndex)
|
||||
let section = sections.get(grouping.key)
|
||||
if (!section) {
|
||||
section = { label: grouping.label, items: [], repo: grouping.repo, repoIds: new Set() }
|
||||
sections.set(grouping.key, section)
|
||||
}
|
||||
section.items.push(item)
|
||||
section.repoIds.add(repoId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the project section for a repo that has no rows to file, or joins the repo to the open one.
|
||||
* `requireRepo` skips repo ids missing from the catalog, which would otherwise read "Unknown".
|
||||
*/
|
||||
function ensureProjectSectionForRepo<Item, R extends ProjectSectionRepo>(
|
||||
sections: Map<string, ProjectSectionEntry<Item, R>>,
|
||||
repoId: string,
|
||||
repoMap: ReadonlyMap<string, R>,
|
||||
projectIndex: ProjectGroupingIndex | null,
|
||||
options: { requireRepo?: boolean; fallbackRepo?: R } = {}
|
||||
): void {
|
||||
const grouping = getProjectGroupingForRepo(repoId, repoMap, projectIndex)
|
||||
if (options.requireRepo && !grouping.repo) {
|
||||
return
|
||||
}
|
||||
const section = sections.get(grouping.key)
|
||||
if (section) {
|
||||
section.repoIds.add(repoId)
|
||||
return
|
||||
}
|
||||
sections.set(grouping.key, {
|
||||
label: grouping.label,
|
||||
items: [],
|
||||
repo: grouping.repo ?? options.fallbackRepo,
|
||||
repoIds: new Set([repoId])
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Project sections in header order, unlabelled (see withRepoSectionDisplayLabels): rows first, then
|
||||
* empty-project placeholders, notice rows' repos (opened with the notice's own repo if the catalog
|
||||
* lacks it), and repos with a workspace being created.
|
||||
*/
|
||||
export function buildProjectSections<
|
||||
Item extends ActivityItem,
|
||||
R extends ProjectSectionRepo
|
||||
>(args: {
|
||||
items: readonly Item[]
|
||||
repoIdOf: (item: Item) => string
|
||||
repoMap: ReadonlyMap<string, R>
|
||||
projectIndex: ProjectGroupingIndex | null
|
||||
placeholderRepoIds: Iterable<string>
|
||||
noticeRepos?: Iterable<readonly [string, R | undefined]>
|
||||
pendingRepoIds?: Iterable<string>
|
||||
repoOrder: ReadonlyMap<string, number> | undefined
|
||||
projectOrderBy: ProjectOrderBy
|
||||
}): [string, ProjectSectionEntry<Item, R>][] {
|
||||
const { repoMap, projectIndex } = args
|
||||
const sections = new Map<string, ProjectSectionEntry<Item, R>>()
|
||||
for (const item of args.items) {
|
||||
addItemToProjectSection(sections, item, args.repoIdOf(item), repoMap, projectIndex)
|
||||
}
|
||||
// Why: repos can arrive before worktree scans, but stale IDs passed by
|
||||
// older snapshots must not render an "Unknown" project header.
|
||||
for (const repoId of args.placeholderRepoIds) {
|
||||
ensureProjectSectionForRepo(sections, repoId, repoMap, projectIndex, { requireRepo: true })
|
||||
}
|
||||
// Why: the default policy removes pinned worktrees from natural groups,
|
||||
// but actionable notice rows still need a project section to render in.
|
||||
for (const [repoId, fallbackRepo] of args.noticeRepos ?? []) {
|
||||
ensureProjectSectionForRepo(sections, repoId, repoMap, projectIndex, { fallbackRepo })
|
||||
}
|
||||
// Why: creating the first worktree in a repo leaves it with no group yet;
|
||||
// ensure one so the in-progress row nests under its repo instead of being dropped.
|
||||
for (const repoId of args.pendingRepoIds ?? []) {
|
||||
ensureProjectSectionForRepo(sections, repoId, repoMap, projectIndex)
|
||||
}
|
||||
return orderProjectSections(sections, repoMap, args.repoOrder, args.projectOrderBy)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user