dsh-remote-vs 0.9.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +15 -4
- package/README.md +13 -3
- package/cordis.patch.yml +10 -5
- package/lib/client.js +728 -94
- package/lib/index.js +2 -2
- package/lib/types/client/composer.d.ts +5 -1
- package/lib/types/client/scm/HistoryPanel.d.ts +5 -0
- package/lib/types/client/scm/actions.d.ts +25 -0
- package/lib/types/client/scm/git-cache.d.ts +93 -0
- package/lib/types/client/scm/panel-cache.d.ts +43 -0
- package/lib/types/client/scm/read-queue.d.ts +74 -0
- package/lib/types/client/store.d.ts +66 -3
- package/lib/types/git-ops.d.ts +38 -0
- package/lib/types/index.d.ts +5 -0
- package/package.json +37 -37
- package/src/client/SettingsHosts.tsx +2 -1
- package/src/client/composer.ts +11 -2
- package/src/client/i18n.ts +10 -2
- package/src/client/scm/HistoryPanel.tsx +61 -11
- package/src/client/scm/ScmPanel.tsx +123 -22
- package/src/client/scm/actions.ts +49 -6
- package/src/client/scm/git-cache.ts +159 -0
- package/src/client/scm/panel-cache.ts +93 -0
- package/src/client/scm/read-queue.ts +151 -0
- package/src/client/store.tsx +360 -60
- package/src/client/vsc/vsc.css +10 -0
- package/src/git-ops.ts +60 -0
- package/src/index.ts +10 -2
package/lib/index.js
CHANGED
|
@@ -4006,7 +4006,7 @@ const Config = z.object({
|
|
|
4006
4006
|
"off",
|
|
4007
4007
|
"shadow",
|
|
4008
4008
|
"enforce"
|
|
4009
|
-
]).default("
|
|
4009
|
+
]).default("enforce").volatile(),
|
|
4010
4010
|
gitTimeoutMs: z.number().step(1).min(MIN_TIMEOUT_MS).max(MAX_TIMEOUT_MS).default(DEFAULT_GIT_TIMEOUT_MS).volatile(),
|
|
4011
4011
|
readTimeoutMs: z.number().step(1).min(MIN_TIMEOUT_MS).max(MAX_TIMEOUT_MS).default(DEFAULT_READ_TIMEOUT_MS).volatile()
|
|
4012
4012
|
});
|
|
@@ -4063,7 +4063,7 @@ function apply(ctx, config) {
|
|
|
4063
4063
|
const worlds = new RemoteWorlds(ctx, {
|
|
4064
4064
|
store,
|
|
4065
4065
|
pool,
|
|
4066
|
-
mode: () => readField(config?.routingMode, "
|
|
4066
|
+
mode: () => readField(config?.routingMode, "enforce")
|
|
4067
4067
|
});
|
|
4068
4068
|
ctx.provide("remoteWorlds", worlds);
|
|
4069
4069
|
ctx.effect(() => () => {
|
|
@@ -95,7 +95,11 @@ interface InputSnapshot {
|
|
|
95
95
|
draft: string;
|
|
96
96
|
draftRev?: number;
|
|
97
97
|
occurrences?: readonly OccurrenceLike[];
|
|
98
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Host submission plane. `sending` exists since DSH 0.2.2-alpha.2: an ordinary
|
|
100
|
+
* message the host has not accepted yet — the editor is read-only there too.
|
|
101
|
+
*/
|
|
102
|
+
phase?: 'plain' | 'adjudicating' | 'claimed' | 'submitting' | 'sending';
|
|
99
103
|
claim?: {
|
|
100
104
|
readonly token: string;
|
|
101
105
|
};
|
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
* repository shows the newest GIT_LOG_MAX commits and is filtered client-side.
|
|
8
8
|
* The docstring here used to promise scroll-triggered paging that does not
|
|
9
9
|
* exist, which is worse than no comment: a reader trusts it.
|
|
10
|
+
*
|
|
11
|
+
* Each repository's last answer is kept in memory (`./panel-cache.ts`), so a
|
|
12
|
+
* switch back to a workspace repaints its commit list at once and the log is
|
|
13
|
+
* re-read behind it — the tab used to blank on every switch, and worse, kept the
|
|
14
|
+
* PREVIOUS repository's rows until the new log arrived.
|
|
10
15
|
*/
|
|
11
16
|
import { type ReactNode } from 'react';
|
|
12
17
|
export declare function HistoryPanel(): ReactNode;
|
|
@@ -16,6 +16,31 @@ export declare class GitOpError extends Error {
|
|
|
16
16
|
export declare function joinRemote(dir: string, rel: string): string;
|
|
17
17
|
/** The repository that encloses a workspace directory (undefined when none). */
|
|
18
18
|
export declare function resolveRepoRoot(alias: string, workspaceRoot: string): Promise<string | undefined>;
|
|
19
|
+
/**
|
|
20
|
+
* The same probe, telling "not a repository" apart from "the probe failed".
|
|
21
|
+
*
|
|
22
|
+
* `git rev-parse --show-toplevel` exits non-zero BOTH when the directory is not
|
|
23
|
+
* inside a work tree and for every way a remote command can fail (host gone,
|
|
24
|
+
* budget blown, sshd refusing, no answer at all). `resolveRepoRoot` folds the two
|
|
25
|
+
* into `undefined`, which is right for the workspace wizard — there "unknown" and
|
|
26
|
+
* "none" both mean "nothing to offer". The SCM panel cannot take that shortcut:
|
|
27
|
+
* treating a failed probe as "no repository" writes an EMPTY snapshot over the
|
|
28
|
+
* cached change list and tells the user their workspace has no git repository.
|
|
29
|
+
* That is exactly the blank state the snapshot cache exists to prevent, so this
|
|
30
|
+
* variant asks a command that KEEPS the answer (`repoProbeDetailCommand`: exit
|
|
31
|
+
* code + merged streams, `LC_ALL=C`) and reports the failure to the caller.
|
|
32
|
+
*/
|
|
33
|
+
export type RepoRootProbe = {
|
|
34
|
+
kind: 'repo';
|
|
35
|
+
root: string;
|
|
36
|
+
} | {
|
|
37
|
+
kind: 'none';
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'failed';
|
|
40
|
+
error: string;
|
|
41
|
+
};
|
|
42
|
+
/** The repository root, or WHY the probe could not answer. */
|
|
43
|
+
export declare function probeRepoRoot(alias: string, workspaceRoot: string): Promise<RepoRootProbe>;
|
|
19
44
|
/** Repositories under a workspace root (the workspace itself sorts first). */
|
|
20
45
|
export declare function findRepos(alias: string, workspaceRoot: string): Promise<string[]>;
|
|
21
46
|
/** Every changed path plus the ignored half. */
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Last-read git state, remembered per workspace AND per repository directory.
|
|
3
|
+
*
|
|
4
|
+
* The complaint this module answers: switching between two remote workspaces
|
|
5
|
+
* blanked the source-control panel. The store dropped `status` / `head` /
|
|
6
|
+
* `repoRoot` on the switch and started a full `git status` again — minutes of
|
|
7
|
+
* "Loading…" on a large repository — even though BOTH workspaces had just been
|
|
8
|
+
* read, so the very data the panel needed was already in memory. It simply had
|
|
9
|
+
* no home: the store keeps one git slice, describing whatever is on screen.
|
|
10
|
+
*
|
|
11
|
+
* A snapshot is that home. It holds everything the panel paints from, keyed by
|
|
12
|
+
* the binding id AND the workspace-relative directory git ran in, because one
|
|
13
|
+
* workspace can carry several repositories and their states must never
|
|
14
|
+
* overwrite each other (the repo picker switches between them). The store
|
|
15
|
+
* hydrates a snapshot the moment a workspace is selected — in the same frame as
|
|
16
|
+
* the dropdown change — and refreshes it in the background; the read replaces
|
|
17
|
+
* the snapshot and repaints only when it is still the current binding's answer.
|
|
18
|
+
*
|
|
19
|
+
* MEMORY ONLY, on purpose: a `-u normal -z --ignored=matching` status of a
|
|
20
|
+
* monorepo is hundreds of kilobytes, and after a page reload it would describe
|
|
21
|
+
* a tree that may have moved on. The cache is an accelerator for switching, not
|
|
22
|
+
* a second source of truth — every value in it was read by a real command.
|
|
23
|
+
*/
|
|
24
|
+
import type { GitStatusDetail } from '../../protocol.ts';
|
|
25
|
+
import type { DirExpansions } from './untracked-dirs.ts';
|
|
26
|
+
/**
|
|
27
|
+
* Branch header, structurally identical to the store's `HeadInfo`.
|
|
28
|
+
*
|
|
29
|
+
* Declared here instead of imported from `store.tsx` so this module stays a
|
|
30
|
+
* standalone leaf: `store` imports it, and a type-only back-import would pull
|
|
31
|
+
* the whole store into the module graph of every gate that touches the cache.
|
|
32
|
+
*/
|
|
33
|
+
export interface GitHead {
|
|
34
|
+
branch: string;
|
|
35
|
+
detached: boolean;
|
|
36
|
+
ahead: number;
|
|
37
|
+
behind: number;
|
|
38
|
+
}
|
|
39
|
+
/** Everything one git read answered, as the panels need it. */
|
|
40
|
+
export interface GitSnapshot {
|
|
41
|
+
/** Enclosing repository root, or undefined when the directory is no repo. */
|
|
42
|
+
repoRoot: string | undefined;
|
|
43
|
+
/** Workspace-relative directory git ran in (the settled one). */
|
|
44
|
+
repoSubdir: string;
|
|
45
|
+
status: GitStatusDetail;
|
|
46
|
+
head: GitHead | undefined;
|
|
47
|
+
/** git answered "not a repository" — a fact, not a failure. */
|
|
48
|
+
repoMissing: boolean;
|
|
49
|
+
/** Expanded untracked directories, repository-relative. */
|
|
50
|
+
untrackedDirs: DirExpansions;
|
|
51
|
+
/** `Date.now()` when the read that produced this snapshot finished. */
|
|
52
|
+
at: number;
|
|
53
|
+
}
|
|
54
|
+
/** Cache key for one (workspace, repository directory) pair. */
|
|
55
|
+
export declare function gitCacheKey(bindingId: string, repoSubdir: string): string;
|
|
56
|
+
/** Split a cache key back into its two halves (undefined when it is not one). */
|
|
57
|
+
export declare function parseGitCacheKey(key: string): {
|
|
58
|
+
bindingId: string;
|
|
59
|
+
repoSubdir: string;
|
|
60
|
+
} | undefined;
|
|
61
|
+
/** The cached read for one (workspace, repository directory), if any. */
|
|
62
|
+
export declare function readGitSnapshot(key: string): GitSnapshot | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Remember one read.
|
|
65
|
+
*
|
|
66
|
+
* Called even when the read is no longer the binding on screen: the walk has
|
|
67
|
+
* already been paid for, and caching it is what makes the NEXT switch back
|
|
68
|
+
* instant instead of another minutes-long wait.
|
|
69
|
+
*
|
|
70
|
+
* An answer OLDER than the one already cached is dropped. Two reads of one
|
|
71
|
+
* workspace can overlap when the repo picker changes the directory mid-read (the
|
|
72
|
+
* settled directory is a second key, see the store's `readGit`), and the older
|
|
73
|
+
* walk may finish last — letting it win would hand the user a change list that
|
|
74
|
+
* predates the one already shown.
|
|
75
|
+
*/
|
|
76
|
+
export declare function writeGitSnapshot(key: string, snapshot: GitSnapshot): void;
|
|
77
|
+
/** Drop every snapshot whose workspace left the binding table. */
|
|
78
|
+
export declare function retainGitSnapshots(bindingIds: ReadonlySet<string>): void;
|
|
79
|
+
/**
|
|
80
|
+
* Write a locally-known status into a cached snapshot.
|
|
81
|
+
*
|
|
82
|
+
* The plotted stage/unstage move is applied to the store's status WITHOUT a
|
|
83
|
+
* repository-wide read (see `applyLocalStage`). Without this write-through the
|
|
84
|
+
* cache would keep the pre-click list, so switching away and back would flip
|
|
85
|
+
* the row back — the panel would show a change the user already staged.
|
|
86
|
+
* A different `repoRoot` means the snapshot is not that repository: refusing is
|
|
87
|
+
* what keeps one workspace's rows out of another's cache.
|
|
88
|
+
*/
|
|
89
|
+
export declare function patchCachedGitStatus(key: string, repoRoot: string | undefined, status: GitStatusDetail): void;
|
|
90
|
+
/** Same write-through for the expanded untracked directories. */
|
|
91
|
+
export declare function patchCachedUntrackedDirs(key: string, repoRoot: string | undefined, dirs: DirExpansions): void;
|
|
92
|
+
/** Test hook: forget every snapshot (the cache gate drives this module alone). */
|
|
93
|
+
export declare function clearGitSnapshots(): void;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-repository caches for the two views that live beside the change list:
|
|
3
|
+
* the source-control panel's side lists (stash / branches / remotes) and the
|
|
4
|
+
* history tab's commit log.
|
|
5
|
+
*
|
|
6
|
+
* Same reasoning as `git-cache.ts`, one level down: the git status snapshot
|
|
7
|
+
* already makes the change list paint instantly when a workspace is selected
|
|
8
|
+
* again, and these caches stop the second wave of blanks. The extras are three
|
|
9
|
+
* more git commands per repository switch; the log is up to 200 commits (`git
|
|
10
|
+
* log` on a deep monorepo is not free). Both are read-only views of state the
|
|
11
|
+
* host already answered — keeping the last answer costs nothing and is what the
|
|
12
|
+
* panels show while their background re-read runs.
|
|
13
|
+
*
|
|
14
|
+
* MEMORY ONLY, same contract as the git snapshots, and every entry is replaced
|
|
15
|
+
* by a real read; nothing is ever synthesised here.
|
|
16
|
+
*/
|
|
17
|
+
import type { GitBranch, GitCommit, GitRemote, GitStash } from '../../protocol.ts';
|
|
18
|
+
/** Stash / branch / remote lists of one repository (one panel refresh). */
|
|
19
|
+
export interface ScmExtrasSnapshot {
|
|
20
|
+
stashes: GitStash[];
|
|
21
|
+
branches: GitBranch[];
|
|
22
|
+
remotes: GitRemote[];
|
|
23
|
+
/** `Date.now()` when the read finished. */
|
|
24
|
+
at: number;
|
|
25
|
+
}
|
|
26
|
+
/** The history tab's commit list of one repository. */
|
|
27
|
+
export interface HistorySnapshot {
|
|
28
|
+
commits: GitCommit[];
|
|
29
|
+
/** `Date.now()` when the read finished. */
|
|
30
|
+
at: number;
|
|
31
|
+
}
|
|
32
|
+
/** Cache key of one repository, as the git panel addresses it: alias + root. */
|
|
33
|
+
export declare function repoCacheKey(alias: string, repoRoot: string): string;
|
|
34
|
+
/** The cached side lists of one repository, if it was read before. */
|
|
35
|
+
export declare function readScmExtras(key: string): ScmExtrasSnapshot | undefined;
|
|
36
|
+
/** Remember one repository's side lists. */
|
|
37
|
+
export declare function writeScmExtras(key: string, snapshot: ScmExtrasSnapshot): void;
|
|
38
|
+
/** The cached commit log of one repository, if it was read before. */
|
|
39
|
+
export declare function readHistorySnapshot(key: string): HistorySnapshot | undefined;
|
|
40
|
+
/** Remember one repository's commit log. */
|
|
41
|
+
export declare function writeHistorySnapshot(key: string, snapshot: HistorySnapshot): void;
|
|
42
|
+
/** Test hook: forget both caches (the panel caches carry no other state). */
|
|
43
|
+
export declare function clearPanelCaches(): void;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One remote read at a time, per key — with two honest contracts.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists
|
|
5
|
+
* ---------------
|
|
6
|
+
* Git reads are expensive on a large repository: `loadStatusDetail` walks the
|
|
7
|
+
* whole index and worktree (minutes on the AOSP monorepo), and several surfaces
|
|
8
|
+
* can ask for one at the same moment — the workspace dropdown, the session
|
|
9
|
+
* follow, the panel's ⟳, and every write made through the plugin. Until now each
|
|
10
|
+
* caller simply started its own command and the newest one won the paint, which
|
|
11
|
+
* meant two concurrent walks of the same tree and a panel that could not tell
|
|
12
|
+
* which answer was current.
|
|
13
|
+
*
|
|
14
|
+
* The queue replaces that with:
|
|
15
|
+
*
|
|
16
|
+
* - `join` — "show me a read of this key". A read already in flight is shared:
|
|
17
|
+
* starting a second one would only duplicate the same walk, so a
|
|
18
|
+
* workspace switch costs nothing when the previous read of that
|
|
19
|
+
* workspace is still running.
|
|
20
|
+
* - `fresh` — "the answer must describe the state as of NOW" (the ⟳ button, and
|
|
21
|
+
* every write: a save, a commit, a stage verb). It never shares a
|
|
22
|
+
* read that started before the call: it waits for the running one to
|
|
23
|
+
* settle and then starts its own. Two `fresh` callers that arrive
|
|
24
|
+
* during the same read share the SINGLE follow-up read, because the
|
|
25
|
+
* second one can see that the follow-up started after its own call.
|
|
26
|
+
*
|
|
27
|
+
* Reads for DIFFERENT keys run in parallel: two workspaces are two repositories,
|
|
28
|
+
* and making one wait for the other would make a switch feel dead. Nothing here
|
|
29
|
+
* paints anything — the caller decides what its answer may overwrite — so the
|
|
30
|
+
* queue carries no store/React knowledge and is directly testable.
|
|
31
|
+
*/
|
|
32
|
+
/** How a caller relates to a read that is already in flight. */
|
|
33
|
+
export type ReadMode = 'join' | 'fresh';
|
|
34
|
+
export declare class ReadQueue {
|
|
35
|
+
private readonly onChange?;
|
|
36
|
+
/** In-flight reads by key (insertion order is irrelevant here). */
|
|
37
|
+
private readonly flights;
|
|
38
|
+
private nextId;
|
|
39
|
+
/**
|
|
40
|
+
* @param onChange - called whenever a read starts or settles, so the owner can
|
|
41
|
+
* derive a "busy" flag from `inflightKeys()` without polling.
|
|
42
|
+
*/
|
|
43
|
+
constructor(onChange?: (() => void) | undefined);
|
|
44
|
+
/** The keys with a read in flight (a caller derives its busy state from this). */
|
|
45
|
+
inflightKeys(): string[];
|
|
46
|
+
/** True while ONE key has a read in flight. */
|
|
47
|
+
isBusy(key: string): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Run `read` for `key`, honouring `mode`.
|
|
50
|
+
*
|
|
51
|
+
* The returned promise settles with this key's answer: a rejection comes from
|
|
52
|
+
* the read the caller ended up waiting on (its own, or the one it joined).
|
|
53
|
+
* `read` must be equivalent for every caller that passes the same key — the
|
|
54
|
+
* queue keeps only the FIRST caller's closure, because a joined read cannot
|
|
55
|
+
* carry a second closure.
|
|
56
|
+
*
|
|
57
|
+
* @param settleMs - hard settle deadline for a read THIS call starts (`0` =
|
|
58
|
+
* unbounded). A joined read keeps the deadline it was started with: its
|
|
59
|
+
* caller already decided how long that read may take.
|
|
60
|
+
*/
|
|
61
|
+
run<T>(key: string, read: () => Promise<T>, mode?: ReadMode, settleMs?: number): Promise<T>;
|
|
62
|
+
/**
|
|
63
|
+
* `fresh` while a read is in flight: wait for it, then read once more.
|
|
64
|
+
*
|
|
65
|
+
* The follow-up is NOT started from here unconditionally, or two `fresh`
|
|
66
|
+
* callers that arrived during the same read would each start one (two
|
|
67
|
+
* concurrent walks, exactly what this class exists to prevent). The second
|
|
68
|
+
* caller instead finds a flight whose id is newer than the one it waited on —
|
|
69
|
+
* a read that started after its own call — and shares it.
|
|
70
|
+
*/
|
|
71
|
+
private after;
|
|
72
|
+
/** Register a flight and run `read`; a synchronous throw becomes a rejection. */
|
|
73
|
+
private start;
|
|
74
|
+
}
|
|
@@ -149,6 +149,8 @@ export interface ExplorerValue {
|
|
|
149
149
|
head: HeadInfo | undefined;
|
|
150
150
|
gitBusy: boolean;
|
|
151
151
|
gitError: string | undefined;
|
|
152
|
+
/** The painted git slice is a cached one; a background refresh is running. */
|
|
153
|
+
gitCached: boolean;
|
|
152
154
|
/** Effective per-git-command budget (ms), for the loading hint. */
|
|
153
155
|
gitDeadlineMs: number;
|
|
154
156
|
tabs: EditorTab[];
|
|
@@ -199,7 +201,16 @@ export interface ExplorerValue {
|
|
|
199
201
|
/** Repositories found under the workspace root, as workspace-relative dirs. */
|
|
200
202
|
discoverRepos: () => Promise<string[]>;
|
|
201
203
|
refreshAll: () => void;
|
|
202
|
-
|
|
204
|
+
/**
|
|
205
|
+
* Re-read git for the binding on screen.
|
|
206
|
+
*
|
|
207
|
+
* `join` shares a read that is already in flight — the right contract for an
|
|
208
|
+
* automatic refresh, since a second walk of the same repository answers
|
|
209
|
+
* exactly the same question. `fresh` (the default) waits for a running read
|
|
210
|
+
* and then reads again: the answer must include whatever the caller just
|
|
211
|
+
* wrote.
|
|
212
|
+
*/
|
|
213
|
+
refreshGit: (mode?: 'join' | 'fresh') => Promise<void>;
|
|
203
214
|
afterWrite: () => Promise<void>;
|
|
204
215
|
/**
|
|
205
216
|
* Stage / unstage paths: the row moves at once, and ONE path-limited status
|
|
@@ -394,8 +405,25 @@ export declare function selectedWorkspaceId(): string;
|
|
|
394
405
|
export declare function setRepoSubdir(subdir: string): Promise<void>;
|
|
395
406
|
/** Repositories found under the workspace root (workspace-relative, root first). */
|
|
396
407
|
export declare function discoverRepos(): Promise<string[]>;
|
|
397
|
-
/**
|
|
398
|
-
|
|
408
|
+
/**
|
|
409
|
+
* Re-read git for the binding on screen.
|
|
410
|
+
*
|
|
411
|
+
* Reads go through `scm/read-queue.ts`, keyed by (binding, repository directory):
|
|
412
|
+
* `join` shares a read that is already in flight — an automatic refresh has no
|
|
413
|
+
* reason to duplicate a walk — while `fresh` waits for it and then reads again,
|
|
414
|
+
* because the caller just wrote something and the answer must include it. Either
|
|
415
|
+
* mode never runs two walks of the same repository at once, which is what lets a
|
|
416
|
+
* background refresh and the ⟳ button coexist instead of racing.
|
|
417
|
+
*
|
|
418
|
+
* The answer is cached under the key the read STARTED with (even when the user
|
|
419
|
+
* has already switched away: the walk is paid for either way, and the cache is
|
|
420
|
+
* what makes the next visit instant) and repaints only while it is still that
|
|
421
|
+
* binding's latest read.
|
|
422
|
+
*
|
|
423
|
+
* @param mode - `fresh` (default) never reuses a read that started before the
|
|
424
|
+
* call; `join` shares a running one (workspace switches, bootstrap).
|
|
425
|
+
*/
|
|
426
|
+
export declare function refreshGit(mode?: 'join' | 'fresh'): Promise<void>;
|
|
399
427
|
/** Refresh everything a working-tree change can invalidate. */
|
|
400
428
|
export declare function afterWrite(): Promise<void>;
|
|
401
429
|
/** The repository the SCM panel is pointed at, or undefined when none. */
|
|
@@ -403,6 +431,25 @@ export declare function scmTarget(): {
|
|
|
403
431
|
alias: string;
|
|
404
432
|
root: string;
|
|
405
433
|
} | undefined;
|
|
434
|
+
/**
|
|
435
|
+
* Read-only view of the git slice, for callers that do not render.
|
|
436
|
+
*
|
|
437
|
+
* The UI reads the same values through `useExplorer()`; this getter exists for
|
|
438
|
+
* the local gates, which drive this module in node with nothing mounted (they
|
|
439
|
+
* need to observe the switch, the cached flag and what a failed read left
|
|
440
|
+
* behind). It hands out primitives only, so no caller can mutate the store
|
|
441
|
+
* through it.
|
|
442
|
+
*/
|
|
443
|
+
export declare function gitState(): {
|
|
444
|
+
workspaceId: string;
|
|
445
|
+
repoRoot: string | undefined;
|
|
446
|
+
repoSubdir: string;
|
|
447
|
+
changes: number;
|
|
448
|
+
gitBusy: boolean;
|
|
449
|
+
gitCached: boolean;
|
|
450
|
+
gitError: string | undefined;
|
|
451
|
+
repoMissing: boolean;
|
|
452
|
+
};
|
|
406
453
|
/**
|
|
407
454
|
* Move paths between the index and the worktree halves of the LOCAL status
|
|
408
455
|
* model — the optimistic update, its rollback, and the correction the probe
|
|
@@ -446,6 +493,22 @@ export declare function stageOptimistic(paths: readonly string[], staged: boolea
|
|
|
446
493
|
* @param dirPath - the directory row's repo-relative path (with its `/`).
|
|
447
494
|
*/
|
|
448
495
|
export declare function toggleUntrackedDir(dirPath: string): Promise<void>;
|
|
496
|
+
/**
|
|
497
|
+
* Hard settle deadline for ONE queued git read (`scm/read-queue.ts`).
|
|
498
|
+
*
|
|
499
|
+
* A read that never settles would keep its key reserved forever: later `join`
|
|
500
|
+
* calls reuse the dead promise, `fresh` callers wait for it, and the busy flag
|
|
501
|
+
* stays up — one hung host call would freeze that key for the rest of the page
|
|
502
|
+
* (the pre-cache code could always re-read, so this is the one regression a
|
|
503
|
+
* shared flight introduces). The bound is generous on purpose: the status read
|
|
504
|
+
* issues its probe and then its two loaders SEQUENTIALLY, so twice the configured
|
|
505
|
+
* git budget plus slack is "stuck", not "slow".
|
|
506
|
+
*
|
|
507
|
+
* Exported because the panels own their own queues (SCM side lists, history log)
|
|
508
|
+
* and must pass the same bound — a queue without one is a permanent lock waiting
|
|
509
|
+
* for a host hiccup.
|
|
510
|
+
*/
|
|
511
|
+
export declare function gitReadSettleMs(): number;
|
|
449
512
|
/** Open (or focus) a remote file in the editor tab strip. */
|
|
450
513
|
export declare function openFile(fullPath: string, options?: {
|
|
451
514
|
line?: number;
|
package/lib/types/git-ops.d.ts
CHANGED
|
@@ -15,6 +15,25 @@ export declare function assertRemoteName(name: string): string;
|
|
|
15
15
|
export declare function assertRemoteUrl(url: string): string;
|
|
16
16
|
/** Resolve the enclosing repository root (fails outside a work tree). */
|
|
17
17
|
export declare function repoProbeCommand(workdir: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Repository probe that keeps the ANSWER (exit code + both streams) on stdout.
|
|
20
|
+
*
|
|
21
|
+
* `repoProbeCommand` throws the exit code and stderr away — everything that is
|
|
22
|
+
* not a root line reads as "nothing". That is fine for the workspace wizard
|
|
23
|
+
* ("nothing to offer") but not for the SCM panel, where "this is not a
|
|
24
|
+
* repository" and "git could not answer" are DIFFERENT states: the first is a
|
|
25
|
+
* fact that may be cached, the second must keep the last known change list and
|
|
26
|
+
* report the reason. So this command always exits 0 and prints
|
|
27
|
+
*
|
|
28
|
+
* RVS_RC=<git exit code>
|
|
29
|
+
* <everything git wrote, stderr merged>
|
|
30
|
+
*
|
|
31
|
+
* `LC_ALL=C` pins git's wording (the client matches `not a git repository`; a
|
|
32
|
+
* localized host would otherwise read as a failure), and merging the streams is
|
|
33
|
+
* what makes that message survive — git writes it to stderr, which the plain
|
|
34
|
+
* probe discards.
|
|
35
|
+
*/
|
|
36
|
+
export declare function repoProbeDetailCommand(workdir: string): string;
|
|
18
37
|
/** Discover repositories under a workspace root (depth-capped). */
|
|
19
38
|
export declare function repoDiscoveryCommand(root: string): string;
|
|
20
39
|
/**
|
|
@@ -143,6 +162,25 @@ export declare function initCommand(workdir: string): string;
|
|
|
143
162
|
export declare function applyPatchCommand(workdir: string, patchPath: string, mode: 'stage' | 'unstage' | 'revert'): string;
|
|
144
163
|
/** Enclosing repository root from `rev-parse --show-toplevel`. */
|
|
145
164
|
export declare function parseRepoRoot(stdout: string): string | undefined;
|
|
165
|
+
/** One strict probe's answer (see `repoProbeDetailCommand`). */
|
|
166
|
+
export type RepoProbeAnswer = {
|
|
167
|
+
kind: 'repo';
|
|
168
|
+
root: string;
|
|
169
|
+
} | {
|
|
170
|
+
kind: 'none';
|
|
171
|
+
} | {
|
|
172
|
+
kind: 'failed';
|
|
173
|
+
error: string;
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* Parse `repoProbeDetailCommand`'s output into the three states the panel needs.
|
|
177
|
+
*
|
|
178
|
+
* "not a git repository" is matched on git's own message rather than on the exit
|
|
179
|
+
* code: `rev-parse` exits 128 for that AND for a directory it cannot enter, so
|
|
180
|
+
* the code alone cannot separate the two. The message is guaranteed English by
|
|
181
|
+
* the command's `LC_ALL=C`.
|
|
182
|
+
*/
|
|
183
|
+
export declare function parseRepoProbe(stdout: string): RepoProbeAnswer;
|
|
146
184
|
/** Repository dirs found under a workspace root. */
|
|
147
185
|
export declare function parseRepoDiscovery(stdout: string): string[];
|
|
148
186
|
/**
|
package/lib/types/index.d.ts
CHANGED
|
@@ -39,6 +39,11 @@ export interface Config {
|
|
|
39
39
|
/**
|
|
40
40
|
* P2 transparent-routing kill-switch: off = pure local passthrough;
|
|
41
41
|
* shadow = log remote decisions but execute locally; enforce = route.
|
|
42
|
+
*
|
|
43
|
+
* The DEFAULT is `enforce`: the plugin exists to execute remote workspaces
|
|
44
|
+
* remotely, and a fresh profile that silently kept executing locally was the
|
|
45
|
+
* most common "why is nothing remote" report. `off`/`shadow` stay available as
|
|
46
|
+
* explicit opt-outs for debugging and for hosts that must stay local.
|
|
42
47
|
*/
|
|
43
48
|
routingMode?: RoutingMode;
|
|
44
49
|
/** Per-command timeout for git work on a remote host (ms). */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-vs",
|
|
3
3
|
"description": "VSCode-style remote development for DSH: SSH host manager (key/password), helper deploy pipeline with version check, remote file explorer + editor + terminal in dsh-better-sidebar.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.10.0",
|
|
5
5
|
"icon": "icon.svg",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"dsh",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"./locale/*.json": "./locale/*.json"
|
|
60
60
|
},
|
|
61
61
|
"engines": {
|
|
62
|
-
"dsh": ">=0.2.
|
|
62
|
+
"dsh": ">=0.2.2-alpha.2 <0.3.0"
|
|
63
63
|
},
|
|
64
64
|
"dsh": {
|
|
65
65
|
"bundle": {
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
"@codemirror/view": "^6.43.14",
|
|
123
123
|
"@deepseek-ai/dsh-fs-ssh": "0.2.1-rc.2",
|
|
124
124
|
"@deepseek-ai/dsh-ssh": "0.2.1-rc.2",
|
|
125
|
-
"@deepseek-ai/schemastery": "3.18.
|
|
125
|
+
"@deepseek-ai/schemastery": "3.18.6-alpha.1",
|
|
126
126
|
"@lezer/highlight": "1.2.3",
|
|
127
127
|
"@xterm/addon-fit": "^0.11.0",
|
|
128
128
|
"@xterm/xterm": "^6.0.0",
|
|
@@ -130,44 +130,44 @@
|
|
|
130
130
|
"zod": "4.4.3"
|
|
131
131
|
},
|
|
132
132
|
"peerDependencies": {
|
|
133
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
134
|
-
"@deepseek-ai/dsh-agent-preset": "^0.2.
|
|
135
|
-
"@deepseek-ai/dsh-bash-local": "^0.2.
|
|
136
|
-
"@deepseek-ai/dsh-file-reference": "^0.2.
|
|
137
|
-
"@deepseek-ai/dsh-fs": "^0.2.
|
|
138
|
-
"@deepseek-ai/dsh-shell": "^0.2.
|
|
139
|
-
"@deepseek-ai/dsh-spill": "^0.2.
|
|
140
|
-
"@deepseek-ai/dsh-subprocess": "^0.2.
|
|
141
|
-
"@deepseek-ai/dsh-tools": "^0.2.
|
|
133
|
+
"@deepseek-ai/cordis": "^4.0.6-alpha.1",
|
|
134
|
+
"@deepseek-ai/dsh-agent-preset": "^0.2.2-alpha.2",
|
|
135
|
+
"@deepseek-ai/dsh-bash-local": "^0.2.2-alpha.2",
|
|
136
|
+
"@deepseek-ai/dsh-file-reference": "^0.2.2-alpha.2",
|
|
137
|
+
"@deepseek-ai/dsh-fs": "^0.2.2-alpha.2",
|
|
138
|
+
"@deepseek-ai/dsh-shell": "^0.2.2-alpha.2",
|
|
139
|
+
"@deepseek-ai/dsh-spill": "^0.2.2-alpha.2",
|
|
140
|
+
"@deepseek-ai/dsh-subprocess": "^0.2.2-alpha.2",
|
|
141
|
+
"@deepseek-ai/dsh-tools": "^0.2.2-alpha.2",
|
|
142
142
|
"react": "^18.2.0",
|
|
143
143
|
"react-dom": "^18.2.0"
|
|
144
144
|
},
|
|
145
145
|
"devDependencies": {
|
|
146
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
147
|
-
"@deepseek-ai/dsh-agent": "0.2.
|
|
148
|
-
"@deepseek-ai/dsh-agent-preset-registry": "0.2.
|
|
149
|
-
"@deepseek-ai/dsh-api-remotes": "0.2.
|
|
150
|
-
"@deepseek-ai/dsh-bash-local": "0.2.
|
|
151
|
-
"@deepseek-ai/dsh-client-locale": "0.2.
|
|
152
|
-
"@deepseek-ai/dsh-client-ui-renderer": "0.2.
|
|
153
|
-
"@deepseek-ai/dsh-client-ui-settings": "0.2.
|
|
154
|
-
"@deepseek-ai/dsh-client-ui-sidebar-right": "0.2.
|
|
155
|
-
"@deepseek-ai/dsh-client-ui-slots": "0.2.
|
|
156
|
-
"@deepseek-ai/dsh-client-ui-workspace": "0.2.
|
|
157
|
-
"@deepseek-ai/dsh-file-reference": "0.2.
|
|
158
|
-
"@deepseek-ai/dsh-fs": "0.2.
|
|
159
|
-
"@deepseek-ai/dsh-host-directory-picker": "0.2.
|
|
160
|
-
"@deepseek-ai/dsh-host-webserver": "0.2.
|
|
161
|
-
"@deepseek-ai/dsh-llm": "0.2.
|
|
162
|
-
"@deepseek-ai/dsh-sandbox": "0.2.
|
|
163
|
-
"@deepseek-ai/dsh-session": "0.2.
|
|
164
|
-
"@deepseek-ai/dsh-shell": "0.2.
|
|
165
|
-
"@deepseek-ai/dsh-spill": "0.2.
|
|
166
|
-
"@deepseek-ai/dsh-subprocess": "0.2.
|
|
167
|
-
"@deepseek-ai/dsh-system-prompt": "0.2.
|
|
168
|
-
"@deepseek-ai/dsh-tools": "0.2.
|
|
169
|
-
"@deepseek-ai/dsh-working-directory": "0.2.
|
|
170
|
-
"@deepseek-ai/dsh-workspace": "0.2.
|
|
146
|
+
"@deepseek-ai/cordis": "^4.0.6-alpha.1",
|
|
147
|
+
"@deepseek-ai/dsh-agent": "0.2.2-alpha.2",
|
|
148
|
+
"@deepseek-ai/dsh-agent-preset-registry": "0.2.2-alpha.2",
|
|
149
|
+
"@deepseek-ai/dsh-api-remotes": "0.2.2-alpha.2",
|
|
150
|
+
"@deepseek-ai/dsh-bash-local": "0.2.2-alpha.2",
|
|
151
|
+
"@deepseek-ai/dsh-client-locale": "0.2.2-alpha.2",
|
|
152
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.2.2-alpha.2",
|
|
153
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.2.2-alpha.2",
|
|
154
|
+
"@deepseek-ai/dsh-client-ui-sidebar-right": "0.2.2-alpha.2",
|
|
155
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.2.2-alpha.2",
|
|
156
|
+
"@deepseek-ai/dsh-client-ui-workspace": "0.2.2-alpha.2",
|
|
157
|
+
"@deepseek-ai/dsh-file-reference": "0.2.2-alpha.2",
|
|
158
|
+
"@deepseek-ai/dsh-fs": "0.2.2-alpha.2",
|
|
159
|
+
"@deepseek-ai/dsh-host-directory-picker": "0.2.2-alpha.2",
|
|
160
|
+
"@deepseek-ai/dsh-host-webserver": "0.2.2-alpha.2",
|
|
161
|
+
"@deepseek-ai/dsh-llm": "0.2.2-alpha.2",
|
|
162
|
+
"@deepseek-ai/dsh-sandbox": "0.2.2-alpha.2",
|
|
163
|
+
"@deepseek-ai/dsh-session": "0.2.2-alpha.2",
|
|
164
|
+
"@deepseek-ai/dsh-shell": "0.2.2-alpha.2",
|
|
165
|
+
"@deepseek-ai/dsh-spill": "0.2.2-alpha.2",
|
|
166
|
+
"@deepseek-ai/dsh-subprocess": "0.2.2-alpha.2",
|
|
167
|
+
"@deepseek-ai/dsh-system-prompt": "0.2.2-alpha.2",
|
|
168
|
+
"@deepseek-ai/dsh-tools": "0.2.2-alpha.2",
|
|
169
|
+
"@deepseek-ai/dsh-working-directory": "0.2.2-alpha.2",
|
|
170
|
+
"@deepseek-ai/dsh-workspace": "0.2.2-alpha.2",
|
|
171
171
|
"@types/node": "^22.20.0",
|
|
172
172
|
"@types/react": "~18.3.1",
|
|
173
173
|
"@types/react-dom": "^18.3.5",
|
|
@@ -22,7 +22,8 @@ import { Notice, Row, hairline, optionStyle, selectStyle } from './ui.tsx'
|
|
|
22
22
|
export function SettingsHostsSection(_props: SettingsSectionOwnerProps): ReactNode {
|
|
23
23
|
const [t] = useState(() => makeT(detectLang()))
|
|
24
24
|
const [hosts, setHosts] = useState<RemoteHostSummary[]>([])
|
|
25
|
-
|
|
25
|
+
/** Pre-load placeholder; matches the plugin's default (nforce) until the host answers. */
|
|
26
|
+
const [mode, setMode] = useState<'off' | 'shadow' | 'enforce'>('enforce')
|
|
26
27
|
const [version, setVersion] = useState('')
|
|
27
28
|
const [saving, setSaving] = useState(false)
|
|
28
29
|
const [error, setError] = useState<string | undefined>(undefined)
|
package/src/client/composer.ts
CHANGED
|
@@ -100,7 +100,11 @@ interface InputSnapshot {
|
|
|
100
100
|
draft: string
|
|
101
101
|
draftRev?: number
|
|
102
102
|
occurrences?: readonly OccurrenceLike[]
|
|
103
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Host submission plane. `sending` exists since DSH 0.2.2-alpha.2: an ordinary
|
|
105
|
+
* message the host has not accepted yet — the editor is read-only there too.
|
|
106
|
+
*/
|
|
107
|
+
phase?: 'plain' | 'adjudicating' | 'claimed' | 'submitting' | 'sending'
|
|
104
108
|
claim?: { readonly token: string }
|
|
105
109
|
}
|
|
106
110
|
|
|
@@ -295,7 +299,7 @@ export function insertDraftReference(
|
|
|
295
299
|
const { actx, input } = resolved
|
|
296
300
|
const snapshot = input.state.getSnapshot()
|
|
297
301
|
if (input.insertReference === undefined || snapshot.draftRev === undefined) return false
|
|
298
|
-
if (snapshot.phase === 'adjudicating' || snapshot.phase === 'submitting') return false
|
|
302
|
+
if (snapshot.phase === 'adjudicating' || snapshot.phase === 'submitting' || snapshot.phase === 'sending') return false
|
|
299
303
|
before = snapshot.draftRev
|
|
300
304
|
const at = appendOffset(snapshot)
|
|
301
305
|
if (at === undefined) return false
|
|
@@ -346,6 +350,11 @@ export function appendToDraft(ctx: Context, sessionId: string, text: string): bo
|
|
|
346
350
|
const { actx, input } = resolved
|
|
347
351
|
const snapshot = input.state.getSnapshot()
|
|
348
352
|
const draft = snapshot.draft
|
|
353
|
+
// The host makes the editor read-only on every submission plane: a slash
|
|
354
|
+
// command being checked, a claimed command running, and (since DSH
|
|
355
|
+
// 0.2.2-alpha.2) an ordinary message waiting for the host to accept it.
|
|
356
|
+
// Writing there would either be refused or land after the send snapshot.
|
|
357
|
+
if (snapshot.phase === 'adjudicating' || snapshot.phase === 'submitting' || snapshot.phase === 'sending') return false
|
|
349
358
|
if ((snapshot.occurrences ?? []).length > 0 || snapshot.phase === 'claimed') {
|
|
350
359
|
const at = appendOffset(snapshot)
|
|
351
360
|
if (snapshot.draftRev === undefined || at === undefined) {
|