From 558f604ce419e161f44577551ca094841cc72620 Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Fri, 6 Mar 2026 11:53:40 +0800 Subject: [PATCH 1/8] feat: init --- packages/vscode/package.json | 66 +++++++++++++++++++++++++++ packages/vscode/src/generated-meta.ts | 61 +++++++++++++++++++++++++ packages/vscode/tsdown.config.ts | 0 pnpm-lock.yaml | 41 +++++++++++++++++ pnpm-workspace.yaml | 4 +- 5 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 packages/vscode/package.json create mode 100644 packages/vscode/src/generated-meta.ts create mode 100644 packages/vscode/tsdown.config.ts diff --git a/packages/vscode/package.json b/packages/vscode/package.json new file mode 100644 index 0000000..7c45639 --- /dev/null +++ b/packages/vscode/package.json @@ -0,0 +1,66 @@ +{ + "publisher": "Anthony Fu", + "name": "vscode-ghfs", + "type": "module", + "version": "0.0.3", + "packageManager": "pnpm@10.30.3", + "description": "GitHub issues/PRs as filesystem, for offline view and operations in batch. Designed for human and agents.", + "author": { + "name": "Vida Xie", + "email": "vida_2020@163.com", + "url": "https://github.com/9romise" + }, + "license": "MIT", + "funding": "https://github.com/sponsors/antfu", + "homepage": "https://github.com/antfu/ghfs/packages/vscode", + "repository": "https://github.com/antfu/ghfs", + "bugs": "https://github.com/antfu/ghfs/issues", + "keywords": [ + "github", + "issues", + "pull requests", + "markdown", + "cli" + ], + "categories": [ + "Other" + ], + "main": "./dist/index.mjs", + "browser": "./dist/index.mjs", + "icon": "res/logo.png", + "extensionKind": [ + "workspace" + ], + "files": [ + "LICENSE.md", + "dist/*", + "res/*" + ], + "engines": { + "vscode": "^1.101.0" + }, + "activationEvents": [ + "workspaceContains:package.json", + "workspaceContains:pnpm-workspace.yaml", + "workspaceContains:.yarnrc.yml" + ], + "contributes": { + "configuration": {} + }, + "scripts": { + "dev": "tsdown --watch", + "build": "tsdown", + "postinstall": "npm run update", + "update": "vscode-ext-gen --scope ghfs", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "vscode:prepublish": "pnpm run build", + "publish": "npx @vscode/vsce publish --no-dependencies", + "package": "npx @vscode/vsce package --no-dependencies" + }, + "devDependencies": { + "@types/vscode": "1.101.0", + "reactive-vscode": "^1.0.0-beta.2", + "vscode-ext-gen": "1.6.0" + } +} diff --git a/packages/vscode/src/generated-meta.ts b/packages/vscode/src/generated-meta.ts new file mode 100644 index 0000000..8997c27 --- /dev/null +++ b/packages/vscode/src/generated-meta.ts @@ -0,0 +1,61 @@ +// This file is generated by `vscode-ext-gen`. Do not modify manually. +// @see https://github.com/antfu/vscode-ext-gen + +// Meta info +export const publisher = 'Anthony Fu' +export const name = 'vscode-ghfs' +export const version = '0.0.3' +export const displayName = undefined +export const description = 'GitHub issues/PRs as filesystem, for offline view and operations in batch. Designed for human and agents.' +export const extensionId = `${publisher}.${name}` + +/** + * Type union of all commands + */ +export type CommandKey = never + +/** + * Commands map registered by `Anthony Fu.vscode-ghfs` + */ +export const commands = { +} satisfies Record + +/** + * Type union of all configs + */ +export type ConfigKey = never + +export interface ConfigKeyTypeMap { +} + +export interface ConfigShorthandMap { +} + +export interface ConfigShorthandTypeMap { +} + +export interface ConfigItem { + key: T + default: ConfigKeyTypeMap[T] +} + +/** + * Configs map registered by `Anthony Fu.vscode-ghfs` + */ +export const configs = { +} + +export interface ScopedConfigKeyTypeMap { +} + +export const scopedConfigs = { + scope: 'ghfs', + defaults: { + } satisfies ScopedConfigKeyTypeMap, +} + +export interface NestedConfigs { +} + +export interface NestedScopedConfigs { +} diff --git a/packages/vscode/tsdown.config.ts b/packages/vscode/tsdown.config.ts new file mode 100644 index 0000000..e69de29 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64dd49d..dd167fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -79,6 +79,18 @@ importers: specifier: ^4.0.18 version: 4.0.18(@types/node@25.3.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + packages/vscode: + devDependencies: + '@types/vscode': + specifier: 1.101.0 + version: 1.101.0 + reactive-vscode: + specifier: ^1.0.0-beta.2 + version: 1.0.0-beta.2(@types/vscode@1.101.0) + vscode-ext-gen: + specifier: 1.6.0 + version: 1.6.0 + packages: '@antfu/eslint-config@7.6.1': @@ -582,6 +594,9 @@ packages: '@quansync/fs@1.0.0': resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + '@reactive-vscode/reactivity@1.0.0-beta.2': + resolution: {integrity: sha512-sVddfw2Fq285HMG/hGiOFyCGOMhtIBw7n0y1QG58W1CGdMBSUtJ7gXvQJuQz2qO3c9M6zY/BNYRmWCQHFbzemg==} + '@rolldown/binding-android-arm64@1.0.0-rc.3': resolution: {integrity: sha512-0T1k9FinuBZ/t7rZ8jN6OpUKPnUjNdYHoj/cESWrQ3ZraAJ4OMm6z7QjSfCxqj8mOp9kTKc1zHK3kGz5vMu+nQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -940,6 +955,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/vscode@1.101.0': + resolution: {integrity: sha512-ZWf0IWa+NGegdW3iU42AcDTFHWW7fApLdkdnBqwYEtHVIBGbTu0ZNQKP/kX3Ds/uMJXIMQNAojHR4vexCEEz5Q==} + '@typescript-eslint/eslint-plugin@8.56.1': resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2064,6 +2082,11 @@ packages: rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + reactive-vscode@1.0.0-beta.2: + resolution: {integrity: sha512-Er94+XaRhRuJEpSRSCuvXW9pyta53R+VEGxdIG+X/hjruT+rg2HTDZDV0RbtQjwI1yWPflDTB903MkiQOn3w5Q==} + peerDependencies: + '@types/vscode': ^1.101.0 + readdirp@5.0.0: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} @@ -2441,6 +2464,10 @@ packages: jsdom: optional: true + vscode-ext-gen@1.6.0: + resolution: {integrity: sha512-oYg4QCx56hs1+EOVInhQMeuO4jKOCHuBrG9uppivGac8Cu9crJ7YKZg8bviDO60iX9IHqX7o5oRIV/Y+f96XwA==} + hasBin: true + vue-eslint-parser@10.4.0: resolution: {integrity: sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2947,6 +2974,8 @@ snapshots: dependencies: quansync: 1.0.0 + '@reactive-vscode/reactivity@1.0.0-beta.2': {} + '@rolldown/binding-android-arm64@1.0.0-rc.3': optional: true @@ -3160,6 +3189,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/vscode@1.101.0': {} + '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -4565,6 +4596,11 @@ snapshots: defu: 6.1.4 destr: 2.0.5 + reactive-vscode@1.0.0-beta.2(@types/vscode@1.101.0): + dependencies: + '@reactive-vscode/reactivity': 1.0.0-beta.2 + '@types/vscode': 1.101.0 + readdirp@5.0.0: {} refa@0.12.1: @@ -4941,6 +4977,11 @@ snapshots: - tsx - yaml + vscode-ext-gen@1.6.0: + dependencies: + cac: 6.7.14 + tinyglobby: 0.2.15 + vue-eslint-parser@10.4.0(eslint@10.0.2(jiti@2.6.1)): dependencies: debug: 4.4.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 7cf6c55..2c49358 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,8 @@ shellEmulator: true - trustPolicy: no-downgrade +packages: + - packages/* + onlyBuiltDependencies: - esbuild - simple-git-hooks From e837aa1bd35321f31872487f3f61382f93348323 Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Fri, 6 Mar 2026 11:54:32 +0800 Subject: [PATCH 2/8] wip: update tsdown config --- packages/vscode/tsdown.config.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/vscode/tsdown.config.ts b/packages/vscode/tsdown.config.ts index e69de29..f317f9c 100644 --- a/packages/vscode/tsdown.config.ts +++ b/packages/vscode/tsdown.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'tsdown' + +export default defineConfig({ + external: ['vscode'], + /// keep-sorted + inlineOnly: [ + '@reactive-vscode/reactivity', + 'reactive-vscode', + ], + minify: 'dce-only', +}) From 5902d8b1a965e8837992874732e93d61675d39c0 Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Fri, 6 Mar 2026 13:55:59 +0800 Subject: [PATCH 3/8] fix: ignore generated-meta --- packages/vscode/.gitignore | 1 + packages/vscode/src/generated-meta.ts | 61 --------------------------- 2 files changed, 1 insertion(+), 61 deletions(-) create mode 100644 packages/vscode/.gitignore delete mode 100644 packages/vscode/src/generated-meta.ts diff --git a/packages/vscode/.gitignore b/packages/vscode/.gitignore new file mode 100644 index 0000000..730984a --- /dev/null +++ b/packages/vscode/.gitignore @@ -0,0 +1 @@ +generated-meta.ts diff --git a/packages/vscode/src/generated-meta.ts b/packages/vscode/src/generated-meta.ts deleted file mode 100644 index 8997c27..0000000 --- a/packages/vscode/src/generated-meta.ts +++ /dev/null @@ -1,61 +0,0 @@ -// This file is generated by `vscode-ext-gen`. Do not modify manually. -// @see https://github.com/antfu/vscode-ext-gen - -// Meta info -export const publisher = 'Anthony Fu' -export const name = 'vscode-ghfs' -export const version = '0.0.3' -export const displayName = undefined -export const description = 'GitHub issues/PRs as filesystem, for offline view and operations in batch. Designed for human and agents.' -export const extensionId = `${publisher}.${name}` - -/** - * Type union of all commands - */ -export type CommandKey = never - -/** - * Commands map registered by `Anthony Fu.vscode-ghfs` - */ -export const commands = { -} satisfies Record - -/** - * Type union of all configs - */ -export type ConfigKey = never - -export interface ConfigKeyTypeMap { -} - -export interface ConfigShorthandMap { -} - -export interface ConfigShorthandTypeMap { -} - -export interface ConfigItem { - key: T - default: ConfigKeyTypeMap[T] -} - -/** - * Configs map registered by `Anthony Fu.vscode-ghfs` - */ -export const configs = { -} - -export interface ScopedConfigKeyTypeMap { -} - -export const scopedConfigs = { - scope: 'ghfs', - defaults: { - } satisfies ScopedConfigKeyTypeMap, -} - -export interface NestedConfigs { -} - -export interface NestedScopedConfigs { -} From 74f89e513c646360492d3a15a40b932783a60909 Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Fri, 6 Mar 2026 15:45:45 +0800 Subject: [PATCH 4/8] wip --- .vscode/launch.json | 17 ++++++++ packages/vscode/README.md | 20 ++++++++++ packages/vscode/package.json | 30 ++++++++++++-- packages/vscode/src/auth.ts | 9 +++++ packages/vscode/src/commands/sync.ts | 41 ++++++++++++++++++++ packages/vscode/src/composables/auto-sync.ts | 37 ++++++++++++++++++ packages/vscode/src/index.ts | 9 +++++ packages/vscode/src/meta.ts | 7 ++++ packages/vscode/src/sync.ts | 31 +++++++++++++++ packages/vscode/tsdown.config.ts | 14 ++++++- pnpm-lock.yaml | 3 ++ 11 files changed, 212 insertions(+), 6 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 packages/vscode/README.md create mode 100644 packages/vscode/src/auth.ts create mode 100644 packages/vscode/src/commands/sync.ts create mode 100644 packages/vscode/src/composables/auto-sync.ts create mode 100644 packages/vscode/src/index.ts create mode 100644 packages/vscode/src/meta.ts create mode 100644 packages/vscode/src/sync.ts diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..c7c3c3e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}/packages/vscode" + ], + "outFiles": [ + "${workspaceFolder}/packages/vscode/dist/**/*.mjs" + ] + } + ] +} diff --git a/packages/vscode/README.md b/packages/vscode/README.md new file mode 100644 index 0000000..7275352 --- /dev/null +++ b/packages/vscode/README.md @@ -0,0 +1,20 @@ +## Commands + + + +| Command | Title | +| ----------- | --------------------- | +| `ghfs.sync` | ghfs: Sync Repository | + + + +## Configs + + + +| Key | Description | Type | Default | +| ------------------------------- | ------------------------------ | --------- | ------- | +| `ghfs.autoSync.enabled` | Enable automatic periodic sync | `boolean` | `false` | +| `ghfs.autoSync.intervalMinutes` | Auto-sync interval in minutes | `number` | `30` | + + diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 7c45639..0648bec 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -1,6 +1,7 @@ { "publisher": "Anthony Fu", "name": "vscode-ghfs", + "displayName": "ghfs", "type": "module", "version": "0.0.3", "packageManager": "pnpm@10.30.3", @@ -40,12 +41,32 @@ "vscode": "^1.101.0" }, "activationEvents": [ - "workspaceContains:package.json", - "workspaceContains:pnpm-workspace.yaml", - "workspaceContains:.yarnrc.yml" + "workspaceContains:.ghfs" ], "contributes": { - "configuration": {} + "commands": [ + { + "command": "ghfs.sync", + "title": "Sync Repository", + "category": "ghfs" + } + ], + "configuration": { + "title": "ghfs", + "properties": { + "ghfs.autoSync.enabled": { + "type": "boolean", + "default": false, + "description": "Enable automatic periodic sync" + }, + "ghfs.autoSync.intervalMinutes": { + "type": "number", + "default": 30, + "minimum": 1, + "description": "Auto-sync interval in minutes" + } + } + } }, "scripts": { "dev": "tsdown --watch", @@ -59,6 +80,7 @@ "package": "npx @vscode/vsce package --no-dependencies" }, "devDependencies": { + "@ghfs/cli": "workspace:*", "@types/vscode": "1.101.0", "reactive-vscode": "^1.0.0-beta.2", "vscode-ext-gen": "1.6.0" diff --git a/packages/vscode/src/auth.ts b/packages/vscode/src/auth.ts new file mode 100644 index 0000000..1fd0133 --- /dev/null +++ b/packages/vscode/src/auth.ts @@ -0,0 +1,9 @@ +import { authentication } from 'vscode' + +export async function readTokenFromVSCode() { + const session = await authentication.getSession('github', ['repo'], { + createIfNone: true, + }) + + return session?.accessToken +} diff --git a/packages/vscode/src/commands/sync.ts b/packages/vscode/src/commands/sync.ts new file mode 100644 index 0000000..d2c0b3c --- /dev/null +++ b/packages/vscode/src/commands/sync.ts @@ -0,0 +1,41 @@ +import { ProgressLocation, window, workspace } from 'vscode' +import { runSync } from '../sync' + +export async function sync(cwd?: string): Promise { + if (!cwd) { + const activeEditor = window.activeTextEditor + + if (!activeEditor) { + window.showWarningMessage('ghfs: Sync failed - no active editor.') + return + } + + const folder = workspace.getWorkspaceFolder(activeEditor.document.uri) + if (!folder) { + window.showWarningMessage('ghfs: Sync failed - no workspace folder.') + return + } + + cwd = folder.uri.path + } + + await window.withProgress( + { + location: ProgressLocation.Notification, + title: 'ghfs: Syncing repository…', + cancellable: true, + }, + async () => { + try { + const summary = await runSync({ cwd }) + window.showInformationMessage( + `ghfs: Sync finished — ${summary.updatedIssues} issues and ${summary.updatedPulls} PRs updated.`, + ) + } + catch (error) { + const message = error instanceof Error ? error.message : String(error) + window.showErrorMessage(`ghfs: Sync failed — ${message}`) + } + }, + ) +} diff --git a/packages/vscode/src/composables/auto-sync.ts b/packages/vscode/src/composables/auto-sync.ts new file mode 100644 index 0000000..bde451a --- /dev/null +++ b/packages/vscode/src/composables/auto-sync.ts @@ -0,0 +1,37 @@ +import { shallowRef, useWorkspaceFolders, watchEffect } from 'reactive-vscode' +import { config } from '../meta' +import { runSync } from '../sync' + +export function useAutoSync(): void { + const workspaceFolders = useWorkspaceFolders() + const isSyncing = shallowRef(false) + + async function doSync(cwd: string): Promise { + if (isSyncing.value) + return + isSyncing.value = true + try { + await runSync({ cwd }) + } + catch { + // auto-sync errors are silently ignored + } + finally { + isSyncing.value = false + } + } + + watchEffect((onCleanup) => { + if (!config.autoSync.enabled || !workspaceFolders.value) + return + + const intervalMs = Math.max(1, config.autoSync.intervalMinutes) * 60_000 + const timers = workspaceFolders.value.map((folder) => { + const cwd = folder.uri.path + + return setInterval(() => doSync(cwd), intervalMs) + }) + + onCleanup(() => timers.forEach(clearInterval)) + }) +} diff --git a/packages/vscode/src/index.ts b/packages/vscode/src/index.ts new file mode 100644 index 0000000..da4fa0f --- /dev/null +++ b/packages/vscode/src/index.ts @@ -0,0 +1,9 @@ +import { defineExtension, useCommand } from 'reactive-vscode' +import { sync } from './commands/sync' +import { useAutoSync } from './composables/auto-sync' +import { commands } from './generated-meta' + +export const { activate, deactivate } = defineExtension(() => { + useCommand(commands.sync, sync) + useAutoSync() +}) diff --git a/packages/vscode/src/meta.ts b/packages/vscode/src/meta.ts new file mode 100644 index 0000000..99e6068 --- /dev/null +++ b/packages/vscode/src/meta.ts @@ -0,0 +1,7 @@ +import type { NestedScopedConfigs } from './generated-meta' +import { defineConfig, defineLogger } from 'reactive-vscode' +import { displayName, scopedConfigs } from './generated-meta' + +export const config = defineConfig(scopedConfigs.scope) + +export const logger = defineLogger(displayName) diff --git a/packages/vscode/src/sync.ts b/packages/vscode/src/sync.ts new file mode 100644 index 0000000..1731276 --- /dev/null +++ b/packages/vscode/src/sync.ts @@ -0,0 +1,31 @@ +import type { SyncSummary } from '../../../src/sync' +import { resolve } from 'pathe' +import { getExecuteFile, resolveConfig } from '../../../src/config/load' +import { resolveRepo } from '../../../src/config/repo' +import { ensureExecuteArtifacts } from '../../../src/execute/schema' +import { syncRepository } from '../../../src/sync' +import { readTokenFromVSCode } from './auth' + +export interface RunSyncOptions { + cwd: string +} + +// See src/cli/commands/sync.ts +export async function runSync(options: RunSyncOptions): Promise { + const config = await resolveConfig({ cwd: options.cwd }) + await ensureExecuteArtifacts(resolve(config.cwd, getExecuteFile(config))) + + const repo = await resolveRepo({ + cwd: config.cwd, + configRepo: config.repo, + interactive: false, + }) + + const token = config.auth.token?.trim() ?? await readTokenFromVSCode() + + return syncRepository({ + config, + repo: repo.repo, + token, + }) +} diff --git a/packages/vscode/tsdown.config.ts b/packages/vscode/tsdown.config.ts index f317f9c..bd8a4c7 100644 --- a/packages/vscode/tsdown.config.ts +++ b/packages/vscode/tsdown.config.ts @@ -4,8 +4,18 @@ export default defineConfig({ external: ['vscode'], /// keep-sorted inlineOnly: [ - '@reactive-vscode/reactivity', - 'reactive-vscode', + 'before-after-hook', + 'bottleneck', + 'fast-content-type-parse', + 'jiti', + 'json-with-bigint', + 'pathe', + 'toad-cache', + 'universal-github-app-jwt', + 'universal-user-agent', + 'yaml', + /octokit/, + /reactive-vscode/, ], minify: 'dce-only', }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd167fa..a89c51f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,6 +81,9 @@ importers: packages/vscode: devDependencies: + '@ghfs/cli': + specifier: workspace:* + version: link:../.. '@types/vscode': specifier: 1.101.0 version: 1.101.0 From 86c87af8fe97a692eb5bde9f3c99ad80d9bdfd3b Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Fri, 6 Mar 2026 16:23:54 +0800 Subject: [PATCH 5/8] fix: correct default workspace folder --- packages/vscode/src/commands/sync.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/vscode/src/commands/sync.ts b/packages/vscode/src/commands/sync.ts index d2c0b3c..2931ce0 100644 --- a/packages/vscode/src/commands/sync.ts +++ b/packages/vscode/src/commands/sync.ts @@ -4,15 +4,12 @@ import { runSync } from '../sync' export async function sync(cwd?: string): Promise { if (!cwd) { const activeEditor = window.activeTextEditor + const folder = activeEditor + ? workspace.getWorkspaceFolder(activeEditor.document.uri) + : workspace.workspaceFolders?.[0] - if (!activeEditor) { - window.showWarningMessage('ghfs: Sync failed - no active editor.') - return - } - - const folder = workspace.getWorkspaceFolder(activeEditor.document.uri) if (!folder) { - window.showWarningMessage('ghfs: Sync failed - no workspace folder.') + window.showWarningMessage('ghfs: Sync failed - workspace folder not found.') return } @@ -22,7 +19,7 @@ export async function sync(cwd?: string): Promise { await window.withProgress( { location: ProgressLocation.Notification, - title: 'ghfs: Syncing repository…', + title: 'ghfs: Syncing repository...', cancellable: true, }, async () => { From a52e52735fc4dbcc8c9152a6512234b5d42da1ef Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Fri, 6 Mar 2026 17:15:55 +0800 Subject: [PATCH 6/8] wip: only sync when editor is active --- packages/vscode/src/composables/auto-sync.ts | 27 +++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/vscode/src/composables/auto-sync.ts b/packages/vscode/src/composables/auto-sync.ts index bde451a..6ba5ee7 100644 --- a/packages/vscode/src/composables/auto-sync.ts +++ b/packages/vscode/src/composables/auto-sync.ts @@ -1,10 +1,13 @@ -import { shallowRef, useWorkspaceFolders, watchEffect } from 'reactive-vscode' +import { shallowRef, useWindowState, useWorkspaceFolders, watchEffect } from 'reactive-vscode' import { config } from '../meta' import { runSync } from '../sync' export function useAutoSync(): void { const workspaceFolders = useWorkspaceFolders() + const { focused, active } = useWindowState() + const isSyncing = shallowRef(false) + const pendingFolders = new Set() async function doSync(cwd: string): Promise { if (isSyncing.value) @@ -21,6 +24,25 @@ export function useAutoSync(): void { } } + function syncIfActive(cwd: string): void { + if (focused.value && active.value) { + pendingFolders.delete(cwd) + doSync(cwd) + } + else { + pendingFolders.add(cwd) + } + } + + watchEffect(() => { + if (!focused.value || !active.value) + return + for (const cwd of pendingFolders) { + pendingFolders.delete(cwd) + doSync(cwd) + } + }) + watchEffect((onCleanup) => { if (!config.autoSync.enabled || !workspaceFolders.value) return @@ -28,8 +50,7 @@ export function useAutoSync(): void { const intervalMs = Math.max(1, config.autoSync.intervalMinutes) * 60_000 const timers = workspaceFolders.value.map((folder) => { const cwd = folder.uri.path - - return setInterval(() => doSync(cwd), intervalMs) + return setInterval(() => syncIfActive(cwd), intervalMs) }) onCleanup(() => timers.forEach(clearInterval)) From 9b650d79c9c57b32ad5d5b06307efa24128335f2 Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Fri, 6 Mar 2026 17:50:14 +0800 Subject: [PATCH 7/8] fix: auto-sync fix: listen workspace changed to setup auto-sync fix: check if valid before setup auto-sync --- packages/vscode/src/auth.ts | 2 +- packages/vscode/src/composables/auto-sync.ts | 69 ++++++++++++++++---- packages/vscode/src/sync.ts | 2 + packages/vscode/src/utils/fs.ts | 19 ++++++ 4 files changed, 78 insertions(+), 14 deletions(-) create mode 100644 packages/vscode/src/utils/fs.ts diff --git a/packages/vscode/src/auth.ts b/packages/vscode/src/auth.ts index 1fd0133..acf54d1 100644 --- a/packages/vscode/src/auth.ts +++ b/packages/vscode/src/auth.ts @@ -5,5 +5,5 @@ export async function readTokenFromVSCode() { createIfNone: true, }) - return session?.accessToken + return session.accessToken } diff --git a/packages/vscode/src/composables/auto-sync.ts b/packages/vscode/src/composables/auto-sync.ts index 6ba5ee7..af850e1 100644 --- a/packages/vscode/src/composables/auto-sync.ts +++ b/packages/vscode/src/composables/auto-sync.ts @@ -1,30 +1,36 @@ -import { shallowRef, useWindowState, useWorkspaceFolders, watchEffect } from 'reactive-vscode' -import { config } from '../meta' +import type { Uri } from 'vscode' +import { computed, shallowRef, useWindowState, watchEffect } from 'reactive-vscode' +import { workspace } from 'vscode' +import { config, logger } from '../meta' import { runSync } from '../sync' +import { isValidWorkspace } from '../utils/fs' export function useAutoSync(): void { - const workspaceFolders = useWorkspaceFolders() const { focused, active } = useWindowState() const isSyncing = shallowRef(false) + const intervalMs = computed(() => Math.max(1, config.autoSync.intervalMinutes) * 60_000) + const pendingFolders = new Set() + const timers = new Map>() - async function doSync(cwd: string): Promise { + async function doSync(cwd: string) { if (isSyncing.value) return isSyncing.value = true try { await runSync({ cwd }) + timers.set(cwd, setTimeout(() => syncIfActive(cwd), intervalMs.value)) } - catch { - // auto-sync errors are silently ignored + catch (err) { + logger.error(`[auto-sync] error: `, err) } finally { isSyncing.value = false } } - function syncIfActive(cwd: string): void { + function syncIfActive(cwd: string) { if (focused.value && active.value) { pendingFolders.delete(cwd) doSync(cwd) @@ -34,6 +40,30 @@ export function useAutoSync(): void { } } + async function addFolder(cwd: Uri) { + if (timers.has(cwd.path)) + return + + if (await isValidWorkspace(cwd)) + return + + syncIfActive(cwd.path) + } + + function removeFolder(cwd: string) { + const timer = timers.get(cwd) + if (timer) { + clearInterval(timer) + timers.delete(cwd) + } + pendingFolders.delete(cwd) + } + + function clearAllTimers() { + for (const cwd of timers.keys()) + removeFolder(cwd) + } + watchEffect(() => { if (!focused.value || !active.value) return @@ -44,15 +74,28 @@ export function useAutoSync(): void { }) watchEffect((onCleanup) => { - if (!config.autoSync.enabled || !workspaceFolders.value) + if (!config.autoSync.enabled) { + clearAllTimers() return + } + + logger.info('[auto-sync] setup') - const intervalMs = Math.max(1, config.autoSync.intervalMinutes) * 60_000 - const timers = workspaceFolders.value.map((folder) => { - const cwd = folder.uri.path - return setInterval(() => syncIfActive(cwd), intervalMs) + for (const folder of workspace.workspaceFolders ?? []) + addFolder(folder.uri) + + const disposable = workspace.onDidChangeWorkspaceFolders((e) => { + logger.info('[auto-sync] workspace folders changed') + for (const added of e.added) + addFolder(added.uri) + for (const removed of e.removed) + removeFolder(removed.uri.path) }) - onCleanup(() => timers.forEach(clearInterval)) + onCleanup(() => { + disposable.dispose() + clearAllTimers() + logger.info('[auto-sync] cleanup') + }) }) } diff --git a/packages/vscode/src/sync.ts b/packages/vscode/src/sync.ts index 1731276..f13db3f 100644 --- a/packages/vscode/src/sync.ts +++ b/packages/vscode/src/sync.ts @@ -5,6 +5,7 @@ import { resolveRepo } from '../../../src/config/repo' import { ensureExecuteArtifacts } from '../../../src/execute/schema' import { syncRepository } from '../../../src/sync' import { readTokenFromVSCode } from './auth' +import { logger } from './meta' export interface RunSyncOptions { cwd: string @@ -12,6 +13,7 @@ export interface RunSyncOptions { // See src/cli/commands/sync.ts export async function runSync(options: RunSyncOptions): Promise { + logger.info(`[sync] run at ${Date.now()}`) const config = await resolveConfig({ cwd: options.cwd }) await ensureExecuteArtifacts(resolve(config.cwd, getExecuteFile(config))) diff --git a/packages/vscode/src/utils/fs.ts b/packages/vscode/src/utils/fs.ts new file mode 100644 index 0000000..308f54b --- /dev/null +++ b/packages/vscode/src/utils/fs.ts @@ -0,0 +1,19 @@ +import { Uri, workspace } from 'vscode' +import { resolveConfig } from '../../../../src/config' + +export async function pathExist(uri: Uri) { + try { + await workspace.fs.stat(uri) + return true + } + catch { + return false + } +} + +export async function isValidWorkspace(cwd: Uri) { + const config = await resolveConfig({ cwd: cwd.path }) + const folder = Uri.joinPath(cwd, config.directory) + + return await pathExist(folder) +} From f8f792cb58072a0008c13db24ed792d562879c82 Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Fri, 6 Mar 2026 17:50:40 +0800 Subject: [PATCH 8/8] docs: update --- packages/vscode/README.md | 14 +++++++++----- packages/vscode/package.json | 6 +++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/vscode/README.md b/packages/vscode/README.md index 7275352..7a864c0 100644 --- a/packages/vscode/README.md +++ b/packages/vscode/README.md @@ -1,10 +1,14 @@ +# ghfs + +GitHub issues/PRs as filesystem, for offline view and operations in batch. Designed for human and agents. + ## Commands -| Command | Title | -| ----------- | --------------------- | -| `ghfs.sync` | ghfs: Sync Repository | +| Command | Title | +| ----------- | -------------- | +| `ghfs.sync` | ghfs: Sync Now | @@ -14,7 +18,7 @@ | Key | Description | Type | Default | | ------------------------------- | ------------------------------ | --------- | ------- | -| `ghfs.autoSync.enabled` | Enable automatic periodic sync | `boolean` | `false` | -| `ghfs.autoSync.intervalMinutes` | Auto-sync interval in minutes | `number` | `30` | +| `ghfs.autoSync.enabled` | Enable automatic periodic sync | `boolean` | `true` | +| `ghfs.autoSync.intervalMinutes` | Auto-sync interval in minutes | `number` | `1440` | diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 0648bec..caadf7d 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -47,7 +47,7 @@ "commands": [ { "command": "ghfs.sync", - "title": "Sync Repository", + "title": "Sync Now", "category": "ghfs" } ], @@ -56,12 +56,12 @@ "properties": { "ghfs.autoSync.enabled": { "type": "boolean", - "default": false, + "default": true, "description": "Enable automatic periodic sync" }, "ghfs.autoSync.intervalMinutes": { "type": "number", - "default": 30, + "default": 1440, "minimum": 1, "description": "Auto-sync interval in minutes" }