paseo-dsh-direct 0.2.7 → 0.2.9
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/OVERVIEW.md +3 -2
- package/README.md +2 -1
- package/README.zh.md +2 -1
- package/client/settings-screen.tsx +101 -0
- package/client/timeline-cards.tsx +50 -0
- package/index.client.tsx +45 -3
- package/index.server.ts +10 -9
- package/package.json +12 -5
- package/server/bridge-client.ts +5 -5
- package/server/dsh-config.ts +65 -0
- package/server/provider.ts +20 -7
- package/server/timeline.ts +104 -2
- package/shared/settings.ts +25 -0
- package/shared/timeline-cards.ts +42 -0
package/OVERVIEW.md
CHANGED
|
@@ -18,8 +18,9 @@ Runs DeepSeek Harness (`dsh`) as a native Paseo agent provider. Paseo clients (d
|
|
|
18
18
|
## Processes and environment
|
|
19
19
|
|
|
20
20
|
- **Child process.** For each session the plugin starts the `dsh` executable as a child process and loads a small bridge plugin into it. The two communicate over the child's stdio.
|
|
21
|
-
- **Executable lookup.** In order: the `PASEO_DSH_EXECUTABLE` environment variable,
|
|
22
|
-
- **Profile.** The plugin launches `dsh --profile paseo`. If `<DSH_HOME>/profiles/paseo` does not exist, it is created once by running `dsh --profile paseo --from-default-profile web --dump-config`. `DSH_HOME` defaults to `~/.dsh`. Setting `PASEO_DSH_PROFILE
|
|
21
|
+
- **Executable lookup.** In order: the `PASEO_DSH_EXECUTABLE` environment variable, the executable field of the DeepSeek Harness settings screen, the Paseo setting `agents.providers.dsh-pi.command`, then `dsh` on `PATH`.
|
|
22
|
+
- **Profile.** The plugin launches `dsh --profile paseo`. If `<DSH_HOME>/profiles/paseo` does not exist, it is created once by running `dsh --profile paseo --from-default-profile web --dump-config`. `DSH_HOME` defaults to `~/.dsh`. Setting `PASEO_DSH_PROFILE`, or the profile field of the DeepSeek Harness settings screen (the variable wins), selects another existing profile instead; a missing one is reported as an error, not created.
|
|
23
23
|
- **Environment variables read by the plugin.** `PASEO_DSH_EXECUTABLE`, `PASEO_DSH_PROFILE`, and `DSH_HOME` (to locate the profile directory).
|
|
24
|
+
- **Settings screen.** Executable and profile can also be set under Settings → DeepSeek Harness; changes apply to new sessions.
|
|
24
25
|
- **Environment passed to `dsh`.** The child inherits the Paseo daemon's environment.
|
|
25
26
|
- **Network.** The plugin makes no network requests itself. Model API calls are made by `dsh` according to its own configuration.
|
package/README.md
CHANGED
|
@@ -33,6 +33,7 @@ Most dsh integrations go through ACP, a generic protocol that every agent can sp
|
|
|
33
33
|
- Your existing dsh sessions can be imported and resumed in Paseo.
|
|
34
34
|
- dsh's slash commands (`/compact`, `/plan`, ...) appear in Paseo's `/` menu.
|
|
35
35
|
- Paseo's usage meter shows dsh's token counts and how full the context window is.
|
|
36
|
+
- Goal changes, plan mode switches and permission preset changes show up in the conversation as small timeline cards.
|
|
36
37
|
|
|
37
38
|
## Versioning
|
|
38
39
|
|
|
@@ -44,7 +45,7 @@ Create a conversation with the **DeepSeek Harness** provider. Models and presets
|
|
|
44
45
|
|
|
45
46
|
The provider supports streamed assistant and reasoning output, tool execution and approval, user questions, model/preset configuration, interruption, and session recovery. It starts DSH with its own `paseo` profile and does not modify Paseo or DSH core.
|
|
46
47
|
|
|
47
|
-
The plugin uses the DSH profile `paseo`. On first start it creates that profile from DSH's official `web` template (`~/.dsh/profiles/paseo`, or under `$DSH_HOME` when set). Model routes, the default model, and permission presets go in `~/.dsh/profiles/paseo/cordis.patch.yml`.
|
|
48
|
+
The plugin uses the DSH profile `paseo`. On first start it creates that profile from DSH's official `web` template (`~/.dsh/profiles/paseo`, or under `$DSH_HOME` when set). Model routes, the default model, and permission presets go in `~/.dsh/profiles/paseo/cordis.patch.yml`. The executable and the profile can be set on the **DeepSeek Harness** screen in Paseo's settings (fields `dsh executable` and `dsh profile`). The executable is chosen in this order: `PASEO_DSH_EXECUTABLE`, the settings screen, `agents.providers.dsh-pi.command` in the Paseo configuration, then `dsh` on `PATH`. The profile is chosen from `PASEO_DSH_PROFILE`, then the settings screen, then `paseo`; a profile named explicitly is not created automatically. Changed settings apply to new sessions; running sessions keep their `dsh`. If the Paseo Daemon has plugin loading disabled, enable `pluginsEnabled` in its configuration before installing the plugin.
|
|
48
49
|
|
|
49
50
|
If a native session is still held by another DSH client (such as a terminal UI), release it there before importing or resuming it from Paseo.
|
|
50
51
|
|
package/README.zh.md
CHANGED
|
@@ -33,6 +33,7 @@ paseo plugin update paseo-dsh-direct --yes # 更新到最新
|
|
|
33
33
|
- 已有的 dsh 会话可以导入 Paseo 接着用。
|
|
34
34
|
- dsh 的斜杠命令(`/compact`、`/plan` 等)出现在 Paseo 的 `/` 菜单里。
|
|
35
35
|
- Paseo 的用量表显示 dsh 的 token 用量和上下文占用。
|
|
36
|
+
- 目标变化、计划模式切换和权限预设变化会在对话里显示为小的时间线卡片。
|
|
36
37
|
|
|
37
38
|
## 版本号
|
|
38
39
|
|
|
@@ -44,7 +45,7 @@ paseo plugin update paseo-dsh-direct --yes # 更新到最新
|
|
|
44
45
|
|
|
45
46
|
Provider 支持连续正文和思考输出、工具执行与审批、用户问答、模型和 preset 配置、中断及会话恢复。它用插件专用的 `paseo` profile 启动 DSH,不修改 Paseo 或 DSH 核心。
|
|
46
47
|
|
|
47
|
-
插件使用名为 `paseo` 的 DSH profile。首次启动时,插件用 DSH 官方的 `web` 模板自动创建它(位置是 `~/.dsh/profiles/paseo`;设置了 `$DSH_HOME` 时在其下)。模型路由、默认模型和权限预设写在 `~/.dsh/profiles/paseo/cordis.patch.yml`。可在 Paseo 配置的 `agents.providers.dsh-pi.command`
|
|
48
|
+
插件使用名为 `paseo` 的 DSH profile。首次启动时,插件用 DSH 官方的 `web` 模板自动创建它(位置是 `~/.dsh/profiles/paseo`;设置了 `$DSH_HOME` 时在其下)。模型路由、默认模型和权限预设写在 `~/.dsh/profiles/paseo/cordis.patch.yml`。可在 Paseo 设置里的 **DeepSeek Harness** 页(`dsh executable`、`dsh profile` 两项)指定可执行文件与 profile。可执行文件的选择顺序:`PASEO_DSH_EXECUTABLE`、设置页、Paseo 配置的 `agents.providers.dsh-pi.command`、`PATH` 里的 `dsh`。profile 的选择顺序:`PASEO_DSH_PROFILE`、设置页、默认的 `paseo`;显式指定的 profile 不会被自动创建。改动对新会话生效,已在运行的会话继续使用原来的 `dsh`。如果 Paseo Daemon 默认关闭插件加载,请先在配置中启用 `pluginsEnabled`。
|
|
48
49
|
|
|
49
50
|
如果原生会话仍被另一个 DSH 客户端(如终端界面)占用,请先在那里释放,再从 Paseo 导入或恢复。
|
|
50
51
|
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useSettings } from "@getpaseo/plugin/client";
|
|
2
|
+
import { ScrollView } from "@getpaseo/plugin/client/react-native";
|
|
3
|
+
import { SettingsAction, SettingsInput, SettingsRow, SettingsSection } from "@getpaseo/plugin/client/ui";
|
|
4
|
+
import { useRef } from "react";
|
|
5
|
+
import { DSH_SETTINGS } from "../shared/settings.js";
|
|
6
|
+
|
|
7
|
+
const EXECUTABLE_HINT =
|
|
8
|
+
"Path or command of dsh. PASEO_DSH_EXECUTABLE overrides this; leave it empty to use the Paseo provider command or dsh on PATH. Changes apply to new sessions.";
|
|
9
|
+
const PROFILE_HINT =
|
|
10
|
+
'Name of the dsh profile to launch. PASEO_DSH_PROFILE overrides this; leave it empty to use the "paseo" profile. Changes apply to new sessions.';
|
|
11
|
+
|
|
12
|
+
/** Edits the dsh executable and profile that the server reads when it launches a bridge. */
|
|
13
|
+
export default function DshSettingsScreen() {
|
|
14
|
+
const settings = useSettings(DSH_SETTINGS);
|
|
15
|
+
const draftState = useRef<{ revision: string | undefined; edits: { executable?: string; profile?: string } }>({
|
|
16
|
+
revision: undefined,
|
|
17
|
+
edits: {},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (settings.status === "loading") {
|
|
21
|
+
return (
|
|
22
|
+
<ScrollView>
|
|
23
|
+
<SettingsSection title="DeepSeek Harness">
|
|
24
|
+
<SettingsRow label="Loading settings" />
|
|
25
|
+
</SettingsSection>
|
|
26
|
+
</ScrollView>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
if (settings.status === "error") {
|
|
30
|
+
return (
|
|
31
|
+
<ScrollView>
|
|
32
|
+
<SettingsSection title="DeepSeek Harness">
|
|
33
|
+
<SettingsRow label="Settings unavailable" error={settings.error} />
|
|
34
|
+
<SettingsAction label="Retry" actionLabel="Retry" onPress={() => void settings.reload()} />
|
|
35
|
+
</SettingsSection>
|
|
36
|
+
</ScrollView>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
if (settings.status === "invalid") {
|
|
40
|
+
return (
|
|
41
|
+
<ScrollView>
|
|
42
|
+
<SettingsSection title="DeepSeek Harness">
|
|
43
|
+
<SettingsRow label="Stored settings are invalid" error={settings.error} hint="They are ignored until you reset them." />
|
|
44
|
+
<SettingsAction label="Reset" actionLabel="Reset" onPress={() => void settings.reset()} disabled={settings.saving} />
|
|
45
|
+
</SettingsSection>
|
|
46
|
+
</ScrollView>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const { revision, values } = settings;
|
|
51
|
+
// Edits belong to the revision they were typed against; a new revision remounts the inputs, so drop them.
|
|
52
|
+
if (draftState.current.revision !== revision) {
|
|
53
|
+
draftState.current = { revision, edits: {} };
|
|
54
|
+
}
|
|
55
|
+
const draft = { current: draftState.current.edits };
|
|
56
|
+
const save = () =>
|
|
57
|
+
void settings.save(
|
|
58
|
+
{
|
|
59
|
+
executable: draft.current.executable ?? values.executable ?? "",
|
|
60
|
+
profile: draft.current.profile ?? values.profile ?? "",
|
|
61
|
+
},
|
|
62
|
+
revision,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<ScrollView>
|
|
67
|
+
<SettingsSection title="DeepSeek Harness">
|
|
68
|
+
<SettingsInput
|
|
69
|
+
key={`executable-${revision}`}
|
|
70
|
+
label="dsh executable"
|
|
71
|
+
hint={EXECUTABLE_HINT}
|
|
72
|
+
placeholder="dsh"
|
|
73
|
+
initialValue={values.executable ?? ""}
|
|
74
|
+
onChangeText={(text) => {
|
|
75
|
+
draft.current.executable = text;
|
|
76
|
+
}}
|
|
77
|
+
disabled={settings.saving}
|
|
78
|
+
/>
|
|
79
|
+
<SettingsInput
|
|
80
|
+
key={`profile-${revision}`}
|
|
81
|
+
label="dsh profile"
|
|
82
|
+
hint={PROFILE_HINT}
|
|
83
|
+
placeholder="paseo"
|
|
84
|
+
initialValue={values.profile ?? ""}
|
|
85
|
+
onChangeText={(text) => {
|
|
86
|
+
draft.current.profile = text;
|
|
87
|
+
}}
|
|
88
|
+
disabled={settings.saving}
|
|
89
|
+
/>
|
|
90
|
+
<SettingsAction
|
|
91
|
+
label="Save"
|
|
92
|
+
hint={settings.saveError ?? undefined}
|
|
93
|
+
error={settings.saveError}
|
|
94
|
+
actionLabel={settings.saving ? "Saving" : "Save"}
|
|
95
|
+
onPress={save}
|
|
96
|
+
disabled={settings.saving}
|
|
97
|
+
/>
|
|
98
|
+
</SettingsSection>
|
|
99
|
+
</ScrollView>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { SettingsRow } from "@getpaseo/plugin/client/ui";
|
|
2
|
+
import {
|
|
3
|
+
type GoalCard,
|
|
4
|
+
type PermissionPresetCard,
|
|
5
|
+
type PlanModeCard,
|
|
6
|
+
} from "../shared/timeline-cards.js";
|
|
7
|
+
|
|
8
|
+
interface CardProps<Data> {
|
|
9
|
+
item: { data: Data };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Text of a goal card. */
|
|
13
|
+
function goalCardText(data: GoalCard): string {
|
|
14
|
+
const objective = data.objective ? `: ${data.objective}` : "";
|
|
15
|
+
switch (data.operation) {
|
|
16
|
+
case "create":
|
|
17
|
+
return `Goal set${objective}`;
|
|
18
|
+
case "edit":
|
|
19
|
+
return `Goal edited${objective}`;
|
|
20
|
+
case "pause":
|
|
21
|
+
return "Goal paused";
|
|
22
|
+
case "resume":
|
|
23
|
+
return "Goal resumed";
|
|
24
|
+
case "complete":
|
|
25
|
+
return "Goal completed";
|
|
26
|
+
case "block":
|
|
27
|
+
return data.reason ? `Goal blocked: ${data.reason}` : "Goal blocked";
|
|
28
|
+
case "clear":
|
|
29
|
+
return "Goal cleared";
|
|
30
|
+
case "continue": {
|
|
31
|
+
const round = data.round === undefined ? "" : ` (round ${data.round}${data.maxRounds === undefined ? "" : `/${data.maxRounds}`})`;
|
|
32
|
+
return `Goal continued${round}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** One-row card for a plan mode switch. */
|
|
38
|
+
export function PlanModeCardView({ item }: CardProps<PlanModeCard>) {
|
|
39
|
+
return <SettingsRow label={item.data.active ? "Plan mode on" : "Plan mode off"} />;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** One-row card for a permission preset change. */
|
|
43
|
+
export function PermissionPresetCardView({ item }: CardProps<PermissionPresetCard>) {
|
|
44
|
+
return <SettingsRow label={`Permission preset: ${item.data.preset}`} />;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** One-row card for a goal change or an automatic goal round. */
|
|
48
|
+
export function GoalCardView({ item }: CardProps<GoalCard>) {
|
|
49
|
+
return <SettingsRow label={goalCardText(item.data)} />;
|
|
50
|
+
}
|
package/index.client.tsx
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
import type { PluginClientContext } from "@getpaseo/plugin/client";
|
|
2
|
+
import { SETTINGS_SCREEN_ICON } from "./shared/settings.js";
|
|
3
|
+
import DshSettingsScreen from "./client/settings-screen.js";
|
|
4
|
+
import {
|
|
5
|
+
GoalCardView,
|
|
6
|
+
PermissionPresetCardView,
|
|
7
|
+
PlanModeCardView,
|
|
8
|
+
} from "./client/timeline-cards.js";
|
|
9
|
+
import {
|
|
10
|
+
CARD_KINDS,
|
|
11
|
+
CARD_VERSION,
|
|
12
|
+
goalCardSchema,
|
|
13
|
+
permissionPresetCardSchema,
|
|
14
|
+
planModeCardSchema,
|
|
15
|
+
} from "./shared/timeline-cards.js";
|
|
2
16
|
|
|
3
|
-
/** Provider presentation
|
|
4
|
-
export default function contribute(
|
|
5
|
-
|
|
17
|
+
/** Provider presentation comes from the server registration; the client adds the dsh settings screen and timeline cards. */
|
|
18
|
+
export default function contribute(client: PluginClientContext) {
|
|
19
|
+
const cleanups = [
|
|
20
|
+
client.addSettingsScreen({
|
|
21
|
+
id: "dsh",
|
|
22
|
+
title: "DeepSeek Harness",
|
|
23
|
+
icon: SETTINGS_SCREEN_ICON,
|
|
24
|
+
Component: DshSettingsScreen,
|
|
25
|
+
}),
|
|
26
|
+
client.addTimelineRenderer({
|
|
27
|
+
kind: CARD_KINDS.planMode,
|
|
28
|
+
version: CARD_VERSION,
|
|
29
|
+
schema: planModeCardSchema,
|
|
30
|
+
Component: PlanModeCardView,
|
|
31
|
+
}),
|
|
32
|
+
client.addTimelineRenderer({
|
|
33
|
+
kind: CARD_KINDS.permissionPreset,
|
|
34
|
+
version: CARD_VERSION,
|
|
35
|
+
schema: permissionPresetCardSchema,
|
|
36
|
+
Component: PermissionPresetCardView,
|
|
37
|
+
}),
|
|
38
|
+
client.addTimelineRenderer({
|
|
39
|
+
kind: CARD_KINDS.goal,
|
|
40
|
+
version: CARD_VERSION,
|
|
41
|
+
schema: goalCardSchema,
|
|
42
|
+
Component: GoalCardView,
|
|
43
|
+
}),
|
|
44
|
+
];
|
|
45
|
+
return async () => {
|
|
46
|
+
await Promise.all(cleanups.map((cleanup) => cleanup()));
|
|
47
|
+
};
|
|
6
48
|
}
|
package/index.server.ts
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
|
2
|
+
import { DSH_SETTINGS } from "./shared/settings.js";
|
|
2
3
|
import { launchDshBridge } from "./server/bridge-client.js";
|
|
4
|
+
import { currentDshConfigured } from "./server/dsh-config.js";
|
|
3
5
|
import { defaultDshCommand } from "./server/dsh-spawn.js";
|
|
4
6
|
import { DSH_BRIDGE_SOURCE } from "./server/generated-bridge.js";
|
|
5
7
|
import { createDshProvider } from "./server/provider.js";
|
|
6
8
|
|
|
7
9
|
export default function contribute(server: PluginServerContext) {
|
|
8
|
-
const
|
|
10
|
+
const settings = server.registerSettings(DSH_SETTINGS);
|
|
11
|
+
// Read per call, not once: a changed setting applies to new sessions and new catalog discovery.
|
|
12
|
+
const configured = () => currentDshConfigured(process.env, settings);
|
|
9
13
|
server.registerProvider(
|
|
10
14
|
createDshProvider({
|
|
11
|
-
|
|
15
|
+
configured,
|
|
12
16
|
command: defaultDshCommand(process.env),
|
|
13
|
-
createBridge: (launch) =>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
executable,
|
|
18
|
-
profile: process.env.PASEO_DSH_PROFILE,
|
|
19
|
-
}),
|
|
17
|
+
createBridge: async (launch) => {
|
|
18
|
+
const { executable, profile } = await configured();
|
|
19
|
+
return launchDshBridge({ ...launch, bridgeSource: DSH_BRIDGE_SOURCE, executable, profile });
|
|
20
|
+
},
|
|
20
21
|
}),
|
|
21
22
|
);
|
|
22
23
|
return () => {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paseo-dsh-direct",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "DeepSeek Harness Direct Provider Plugin for Paseo",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"index.server.ts",
|
|
20
20
|
"index.client.tsx",
|
|
21
21
|
"server",
|
|
22
|
+
"client",
|
|
22
23
|
"shared",
|
|
23
24
|
"scripts/build.mjs",
|
|
24
25
|
"scripts/install-lefthook.mjs",
|
|
@@ -27,14 +28,16 @@
|
|
|
27
28
|
"dsh.png",
|
|
28
29
|
"OVERVIEW.md",
|
|
29
30
|
"!server/**/*.test.ts",
|
|
31
|
+
"!client/**/*.test.ts",
|
|
30
32
|
"!server/generated-bridge.ts",
|
|
31
33
|
"!server/generated-version.ts"
|
|
32
34
|
],
|
|
33
35
|
"peerDependencies": {
|
|
34
|
-
"@getpaseo/client": "0.
|
|
35
|
-
"@getpaseo/protocol": "0.
|
|
36
|
+
"@getpaseo/client": "0.12.0",
|
|
37
|
+
"@getpaseo/protocol": "0.12.0",
|
|
36
38
|
"react": "~19.1.0",
|
|
37
|
-
"react-native": ">=0.81.5"
|
|
39
|
+
"react-native": ">=0.81.5",
|
|
40
|
+
"zod": "^4.4.3"
|
|
38
41
|
},
|
|
39
42
|
"peerDependenciesMeta": {
|
|
40
43
|
"@getpaseo/client": {
|
|
@@ -48,12 +51,16 @@
|
|
|
48
51
|
},
|
|
49
52
|
"react-native": {
|
|
50
53
|
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"zod": {
|
|
56
|
+
"optional": true
|
|
51
57
|
}
|
|
52
58
|
},
|
|
53
59
|
"devDependencies": {
|
|
54
|
-
"@getpaseo/plugin": "0.
|
|
60
|
+
"@getpaseo/plugin": "0.12.0",
|
|
55
61
|
"@types/mdast": "^4.0.4",
|
|
56
62
|
"@types/node": "^24.0.0",
|
|
63
|
+
"@types/react": "~19.2.0",
|
|
57
64
|
"esbuild": "^0.25.0",
|
|
58
65
|
"lefthook": "^2.1.9",
|
|
59
66
|
"mdast-util-from-markdown": "^2.0.3",
|
package/server/bridge-client.ts
CHANGED
|
@@ -62,7 +62,7 @@ export interface SessionLaunch {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export interface LaunchBridgeOptions extends SessionLaunch {
|
|
65
|
-
/** The `PASEO_DSH_EXECUTABLE
|
|
65
|
+
/** The configured executable (`PASEO_DSH_EXECUTABLE`, else the plugin setting); it takes precedence over `providerLaunch.command`. */
|
|
66
66
|
executable?: string;
|
|
67
67
|
profile?: string;
|
|
68
68
|
bridgeSource: string;
|
|
@@ -77,7 +77,7 @@ interface PendingRequest {
|
|
|
77
77
|
|
|
78
78
|
const MAX_STDERR = 16_384;
|
|
79
79
|
|
|
80
|
-
/** The profile the plugin launches unless `PASEO_DSH_PROFILE` names another one. */
|
|
80
|
+
/** The profile the plugin launches unless `PASEO_DSH_PROFILE` or the plugin setting names another one. */
|
|
81
81
|
export const DEFAULT_PROFILE = "paseo";
|
|
82
82
|
/** The official DSH profile template the default profile is created from. */
|
|
83
83
|
export const DEFAULT_PROFILE_TEMPLATE = "web";
|
|
@@ -142,7 +142,7 @@ export async function ensureDshProfile(options: {
|
|
|
142
142
|
throw new Error(
|
|
143
143
|
`DSH profile "${options.profile}" does not exist at ${directory}. ` +
|
|
144
144
|
`Create it with \`dsh --profile ${options.profile} --from-default-profile ${DEFAULT_PROFILE_TEMPLATE} --dump-config\`, ` +
|
|
145
|
-
`or unset PASEO_DSH_PROFILE to use the "${DEFAULT_PROFILE}" profile.`,
|
|
145
|
+
`or clear the profile in the DeepSeek Harness plugin settings and unset PASEO_DSH_PROFILE to use the "${DEFAULT_PROFILE}" profile.`,
|
|
146
146
|
);
|
|
147
147
|
}
|
|
148
148
|
// Concurrent first launches share one creation run.
|
|
@@ -222,8 +222,8 @@ export interface DshLaunch {
|
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
/**
|
|
225
|
-
* The dsh launch, by precedence: a non-blank `PASEO_DSH_EXECUTABLE
|
|
226
|
-
*
|
|
225
|
+
* The dsh launch, by precedence: a non-blank configured executable (`PASEO_DSH_EXECUTABLE`,
|
|
226
|
+
* else the plugin setting; see `mergeDshConfigured`), then the `launch` Paseo
|
|
227
227
|
* resolved from `command` / `agents.providers.dsh-pi.command`, then `dsh` from
|
|
228
228
|
* `PATH`. `launch.args` belong to `launch.command`, so they are dropped when
|
|
229
229
|
* the environment variable wins.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** The dsh launch choices that outrank Paseo's own `launch` and the built-in defaults. */
|
|
2
|
+
export interface DshConfigured {
|
|
3
|
+
/** A non-blank dsh executable, or undefined when neither the environment nor the settings name one. */
|
|
4
|
+
executable?: string;
|
|
5
|
+
/** A non-blank profile name, or undefined to use the default profile. */
|
|
6
|
+
profile?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** The two values of the plugin settings screen; blank means unset. */
|
|
10
|
+
export interface DshSettingsValues {
|
|
11
|
+
executable?: string;
|
|
12
|
+
profile?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const nonBlank = (value: string | undefined): string | undefined => value?.trim() || undefined;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Merges the environment and the settings screen: a non-blank
|
|
19
|
+
* `PASEO_DSH_EXECUTABLE` / `PASEO_DSH_PROFILE` wins over the setting of the
|
|
20
|
+
* same name. The result is then resolved against Paseo's `launch` and the
|
|
21
|
+
* default profile by `resolveDshLaunch` and `launchDshBridge`.
|
|
22
|
+
*/
|
|
23
|
+
export function mergeDshConfigured(
|
|
24
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
25
|
+
settings: DshSettingsValues | undefined,
|
|
26
|
+
): DshConfigured {
|
|
27
|
+
const executable = nonBlank(env.PASEO_DSH_EXECUTABLE) ?? nonBlank(settings?.executable);
|
|
28
|
+
const profile = nonBlank(env.PASEO_DSH_PROFILE) ?? nonBlank(settings?.profile);
|
|
29
|
+
return { ...(executable ? { executable } : {}), ...(profile ? { profile } : {}) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The slice of `PluginSettings.read()` this module needs. */
|
|
33
|
+
export interface DshSettingsSource {
|
|
34
|
+
read(): Promise<
|
|
35
|
+
| { status: "ready"; values: DshSettingsValues }
|
|
36
|
+
| { status: "invalid"; error: string }
|
|
37
|
+
| { status: "error"; error: string }
|
|
38
|
+
>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Reads the current settings. Invalid or unreadable settings count as unset,
|
|
43
|
+
* with one line on stderr, so a bad setting never blocks launching dsh.
|
|
44
|
+
*/
|
|
45
|
+
export async function readDshSettings(
|
|
46
|
+
source: DshSettingsSource,
|
|
47
|
+
warn: (line: string) => void = (line) => process.stderr.write(`${line}\n`),
|
|
48
|
+
): Promise<DshSettingsValues | undefined> {
|
|
49
|
+
const state = await source.read().catch((error: unknown) => ({
|
|
50
|
+
status: "error" as const,
|
|
51
|
+
error: error instanceof Error ? error.message : String(error),
|
|
52
|
+
}));
|
|
53
|
+
if (state.status === "ready") return state.values;
|
|
54
|
+
warn(`[paseo-dsh-direct] ignoring the dsh plugin settings (${state.status}): ${state.error}`);
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The configuration the provider uses right now: environment first, then the current settings. */
|
|
59
|
+
export async function currentDshConfigured(
|
|
60
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
61
|
+
source: DshSettingsSource,
|
|
62
|
+
warn?: (line: string) => void,
|
|
63
|
+
): Promise<DshConfigured> {
|
|
64
|
+
return mergeDshConfigured(env, await readDshSettings(source, warn));
|
|
65
|
+
}
|
package/server/provider.ts
CHANGED
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
} from "../shared/bridge-protocol.js";
|
|
26
26
|
import { slashLine } from "../shared/bridge-protocol.js";
|
|
27
27
|
import {
|
|
28
|
+
DEFAULT_PROFILE,
|
|
28
29
|
describeDshLaunch,
|
|
29
30
|
resolveDshLaunch,
|
|
30
31
|
type BridgeMcpServer,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
type DshLaunch,
|
|
33
34
|
type SessionLaunch,
|
|
34
35
|
} from "./bridge-client.js";
|
|
36
|
+
import type { DshConfigured } from "./dsh-config.js";
|
|
35
37
|
import { detectDshVersion } from "./dsh-version.js";
|
|
36
38
|
import { PLUGIN_VERSION } from "./plugin-version.js";
|
|
37
39
|
import { displayText, toPromptParts } from "./prompt-content.js";
|
|
@@ -50,6 +52,7 @@ const CAPABILITIES = [
|
|
|
50
52
|
"session.persistence",
|
|
51
53
|
"permission",
|
|
52
54
|
"permission.tool_policy",
|
|
55
|
+
"timeline.plugin",
|
|
53
56
|
] as const;
|
|
54
57
|
const PERMISSION_PRESET_SETTING = "permissionPreset";
|
|
55
58
|
const PLAN_MODE_SETTING = "planMode";
|
|
@@ -61,8 +64,12 @@ type DetectVersion = (
|
|
|
61
64
|
|
|
62
65
|
interface DshProviderOptions {
|
|
63
66
|
createBridge(launch?: SessionLaunch): Promise<DshBridge>;
|
|
64
|
-
/**
|
|
65
|
-
|
|
67
|
+
/**
|
|
68
|
+
* The environment and settings-screen choices, read on every call so a changed
|
|
69
|
+
* setting applies to new sessions. The executable outranks the `launch` Paseo
|
|
70
|
+
* resolves; `status()`, the catalog cache key and `createBridge` use the same values.
|
|
71
|
+
*/
|
|
72
|
+
configured?: () => Promise<DshConfigured> | DshConfigured;
|
|
66
73
|
/** The command registered with Paseo; defaults to `dsh` (see `defaultDshCommand`). */
|
|
67
74
|
command?: string;
|
|
68
75
|
/** Detects the local dsh version for the startup compatibility check; defaults to spawning `<exe> --version`. */
|
|
@@ -103,12 +110,16 @@ export function createDshProvider(options: DshProviderOptions): ProviderRegistra
|
|
|
103
110
|
label: "DeepSeek Harness",
|
|
104
111
|
description: "Direct connection to the local DeepSeek Harness (dsh)",
|
|
105
112
|
icon: "dsh.svg",
|
|
106
|
-
getCatalogCacheKey: async (
|
|
107
|
-
|
|
113
|
+
getCatalogCacheKey: async (request) => {
|
|
114
|
+
const configured = (await options.configured?.()) ?? {};
|
|
115
|
+
const launch = describeDshLaunch(resolveDshLaunch(configured.executable, request.launch));
|
|
116
|
+
const scope = request.scope === "workspace" ? request.cwd : "global";
|
|
117
|
+
return JSON.stringify(["dsh-pi", scope, launch, configured.profile ?? DEFAULT_PROFILE]);
|
|
118
|
+
},
|
|
108
119
|
command: [options.command ?? "dsh"],
|
|
109
120
|
async status(request) {
|
|
110
121
|
return dshStatus(
|
|
111
|
-
resolveDshLaunch(options.executable, request?.launch),
|
|
122
|
+
resolveDshLaunch((await options.configured?.())?.executable, request?.launch),
|
|
112
123
|
request?.launch?.env,
|
|
113
124
|
options.detectDshVersion ?? detectDshVersion,
|
|
114
125
|
);
|
|
@@ -153,7 +164,7 @@ async function dshStatus(
|
|
|
153
164
|
available: false,
|
|
154
165
|
diagnostic:
|
|
155
166
|
`Could not run "${describeDshLaunch(launch)} --version". Install dsh, ` +
|
|
156
|
-
`set agents.providers.dsh-pi.command in Paseo config, or set PASEO_DSH_EXECUTABLE, ` +
|
|
167
|
+
`set agents.providers.dsh-pi.command in Paseo config, set the executable in the DeepSeek Harness plugin settings, or set PASEO_DSH_EXECUTABLE, ` +
|
|
157
168
|
`then restart the Paseo daemon.`,
|
|
158
169
|
};
|
|
159
170
|
}
|
|
@@ -458,7 +469,7 @@ async function openSession(
|
|
|
458
469
|
open: opened,
|
|
459
470
|
bridge,
|
|
460
471
|
releaseBridgeListeners: [],
|
|
461
|
-
projector: new TimelineProjector(input.sessionId),
|
|
472
|
+
projector: new TimelineProjector(input.sessionId, state.capabilities.includes("timeline.plugin")),
|
|
462
473
|
activeTurnId: null,
|
|
463
474
|
cancelRequested: false,
|
|
464
475
|
dispatch: null,
|
|
@@ -494,6 +505,8 @@ async function openSession(
|
|
|
494
505
|
for (const event of session.open.events) {
|
|
495
506
|
for (const projected of session.projector.projectEvent(event)) state.emit(projected);
|
|
496
507
|
}
|
|
508
|
+
} else {
|
|
509
|
+
session.projector.seed(session.open.events);
|
|
497
510
|
}
|
|
498
511
|
await warnOnDshVersionMismatch(session, state);
|
|
499
512
|
state.emit({ type: "session.ready", requestId: input.requestId, sessionId: input.sessionId });
|
package/server/timeline.ts
CHANGED
|
@@ -4,6 +4,12 @@ import type {
|
|
|
4
4
|
ProviderToolCallDetail,
|
|
5
5
|
} from "@getpaseo/plugin/server/provider";
|
|
6
6
|
import type { DshSessionEvent, DshStreamFrame, JsonObject } from "../shared/bridge-protocol.js";
|
|
7
|
+
import {
|
|
8
|
+
CARD_KINDS,
|
|
9
|
+
CARD_VERSION,
|
|
10
|
+
PLUGIN_ID,
|
|
11
|
+
type GoalCard,
|
|
12
|
+
} from "../shared/timeline-cards.js";
|
|
7
13
|
import { attachmentLabel } from "./prompt-content.js";
|
|
8
14
|
import { classifyTurnEnd } from "./turn-end.js";
|
|
9
15
|
import { arrayValue, numberValue, objectValue, stringValue } from "./values.js";
|
|
@@ -26,7 +32,27 @@ export class TimelineProjector {
|
|
|
26
32
|
{ attemptId: string; ordinal: number }
|
|
27
33
|
>();
|
|
28
34
|
|
|
29
|
-
|
|
35
|
+
private planActive = false;
|
|
36
|
+
private lastPreset: string | undefined;
|
|
37
|
+
private readonly goals = new Map<string, { objective?: string; maxRounds?: number }>();
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param paseoSessionId Paseo session id stamped on every projected event.
|
|
41
|
+
* @param pluginCards Whether the host negotiated `timeline.plugin`; when false no cards are emitted.
|
|
42
|
+
*/
|
|
43
|
+
constructor(
|
|
44
|
+
private readonly paseoSessionId: string,
|
|
45
|
+
private readonly pluginCards = false,
|
|
46
|
+
) {}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Records plan mode, permission preset and goal objectives from history that is
|
|
50
|
+
* not replayed, so the first live change is compared against the right value.
|
|
51
|
+
* @param events DSH durable events of the resumed session, oldest first.
|
|
52
|
+
*/
|
|
53
|
+
seed(events: readonly DshSessionEvent[]): void {
|
|
54
|
+
for (const event of events) this.projectCards(event);
|
|
55
|
+
}
|
|
30
56
|
|
|
31
57
|
projectEvent(
|
|
32
58
|
event: DshSessionEvent,
|
|
@@ -35,7 +61,14 @@ export class TimelineProjector {
|
|
|
35
61
|
const timestamp = toTimestamp(event.time);
|
|
36
62
|
switch (event.type) {
|
|
37
63
|
case "user/message":
|
|
38
|
-
return
|
|
64
|
+
return [
|
|
65
|
+
...this.projectCards(event, timestamp),
|
|
66
|
+
...this.projectUserMessage(event, timestamp),
|
|
67
|
+
];
|
|
68
|
+
case "plan/mode":
|
|
69
|
+
case "permission/preset":
|
|
70
|
+
case "goal/change":
|
|
71
|
+
return this.projectCards(event, timestamp);
|
|
39
72
|
case "command/run":
|
|
40
73
|
return this.projectCommandRun(event, options.clientMessageId, timestamp);
|
|
41
74
|
case "command/done":
|
|
@@ -208,6 +241,75 @@ export class TimelineProjector {
|
|
|
208
241
|
return projected;
|
|
209
242
|
}
|
|
210
243
|
|
|
244
|
+
/** Updates card state for `event` and returns the plugin cards it produces (none when cards are off). */
|
|
245
|
+
private projectCards(event: DshSessionEvent, timestamp?: string): SessionEvent[] {
|
|
246
|
+
const cards: Array<{ id: string; kind: string; data: Record<string, string | number | boolean> }> = [];
|
|
247
|
+
const data = event.data;
|
|
248
|
+
if (event.type === "plan/mode" && typeof data.active === "boolean") {
|
|
249
|
+
if (data.active !== this.planActive) {
|
|
250
|
+
this.planActive = data.active;
|
|
251
|
+
cards.push({ id: `plan-mode:${event.seq}`, kind: CARD_KINDS.planMode, data: { active: data.active } });
|
|
252
|
+
}
|
|
253
|
+
} else if (event.type === "permission/preset" && typeof data.preset === "string") {
|
|
254
|
+
const previous = this.lastPreset;
|
|
255
|
+
this.lastPreset = data.preset;
|
|
256
|
+
if (previous !== undefined && previous !== data.preset) {
|
|
257
|
+
cards.push({ id: `permission-preset:${event.seq}`, kind: CARD_KINDS.permissionPreset, data: { preset: data.preset } });
|
|
258
|
+
}
|
|
259
|
+
} else if (event.type === "goal/change") {
|
|
260
|
+
const card = this.goalChangeCard(data);
|
|
261
|
+
if (card) cards.push({ id: `goal:${event.seq}`, kind: CARD_KINDS.goal, data: { ...card } });
|
|
262
|
+
} else if (event.type === "user/message") {
|
|
263
|
+
const source = objectValue(data.source);
|
|
264
|
+
const round = numberValue(source?.round);
|
|
265
|
+
const goalId = stringValue(source?.goalId);
|
|
266
|
+
if (source?.kind === "goal" && round !== undefined && round >= 1) {
|
|
267
|
+
const known = goalId ? this.goals.get(goalId) : undefined;
|
|
268
|
+
const card: GoalCard = {
|
|
269
|
+
operation: "continue",
|
|
270
|
+
round,
|
|
271
|
+
...(known?.maxRounds !== undefined ? { maxRounds: known.maxRounds } : {}),
|
|
272
|
+
...(known?.objective !== undefined ? { objective: known.objective } : {}),
|
|
273
|
+
};
|
|
274
|
+
cards.push({ id: `goal-continue:${event.seq}`, kind: CARD_KINDS.goal, data: { ...card } });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (!this.pluginCards) return [];
|
|
278
|
+
return cards.map((card) =>
|
|
279
|
+
timeline(
|
|
280
|
+
this.paseoSessionId,
|
|
281
|
+
{ type: "plugin", id: card.id, pluginId: PLUGIN_ID, kind: card.kind, version: CARD_VERSION, data: card.data },
|
|
282
|
+
timestamp,
|
|
283
|
+
),
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private goalChangeCard(data: JsonObject): GoalCard | undefined {
|
|
288
|
+
const operation = stringValue(data.operation);
|
|
289
|
+
if (
|
|
290
|
+
operation !== "create" && operation !== "edit" && operation !== "pause" &&
|
|
291
|
+
operation !== "resume" && operation !== "complete" && operation !== "block" &&
|
|
292
|
+
operation !== "clear"
|
|
293
|
+
) {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
const goal = objectValue(operation === "clear" ? data.cleared : data.goal);
|
|
297
|
+
const goalId = stringValue(goal?.id);
|
|
298
|
+
if (operation !== "clear" && goalId) {
|
|
299
|
+
this.goals.set(goalId, {
|
|
300
|
+
objective: stringValue(goal?.objective),
|
|
301
|
+
maxRounds: numberValue(goal?.maxGoalRounds),
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
const objective = stringValue(goal?.objective) ?? (goalId ? this.goals.get(goalId)?.objective : undefined);
|
|
305
|
+
const reason = operation === "block" ? stringValue(objectValue(goal?.blockedReason)?.message) : undefined;
|
|
306
|
+
return {
|
|
307
|
+
operation,
|
|
308
|
+
...(objective !== undefined ? { objective } : {}),
|
|
309
|
+
...(reason !== undefined ? { reason } : {}),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
211
313
|
private projectUserMessage(event: DshSessionEvent, timestamp?: string): SessionEvent[] {
|
|
212
314
|
const data = event.data;
|
|
213
315
|
const source = objectValue(data.source);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineSettings } from "@getpaseo/plugin";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Host-scoped plugin settings for the dsh launch. Both fields are optional; an
|
|
6
|
+
* empty or whitespace-only value means unset. The server reads them when a
|
|
7
|
+
* bridge is launched, and the client settings screen edits them.
|
|
8
|
+
*/
|
|
9
|
+
export const DSH_SETTINGS = defineSettings({
|
|
10
|
+
id: "dsh",
|
|
11
|
+
scope: "host",
|
|
12
|
+
version: 1,
|
|
13
|
+
schema: z.object({
|
|
14
|
+
/** Path or command of the dsh executable. */
|
|
15
|
+
executable: z.string().optional(),
|
|
16
|
+
/** Name of the dsh profile the plugin launches. */
|
|
17
|
+
profile: z.string().optional(),
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Icon of the dsh settings screen. Paseo resolves plugin icons by the exact export name of
|
|
23
|
+
* `lucide-react-native` (PascalCase) and rejects the whole client contribution otherwise.
|
|
24
|
+
*/
|
|
25
|
+
export const SETTINGS_SCREEN_ICON = "Settings";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/** Plugin id from paseo-plugin.json; Paseo matches timeline plugin items by it, not by the provider id. */
|
|
4
|
+
export const PLUGIN_ID = "paseo-dsh-direct";
|
|
5
|
+
|
|
6
|
+
/** Schema version shared by every card kind below. */
|
|
7
|
+
export const CARD_VERSION = 1;
|
|
8
|
+
|
|
9
|
+
/** Plan mode switched on or off. */
|
|
10
|
+
export const planModeCardSchema = z.object({ active: z.boolean() });
|
|
11
|
+
|
|
12
|
+
/** Permission preset changed to `preset`. */
|
|
13
|
+
export const permissionPresetCardSchema = z.object({ preset: z.string() });
|
|
14
|
+
|
|
15
|
+
/** Goal operations from `goal/change`, plus `continue` for an automatic goal round. */
|
|
16
|
+
export const goalCardSchema = z.object({
|
|
17
|
+
operation: z.enum([
|
|
18
|
+
"create",
|
|
19
|
+
"edit",
|
|
20
|
+
"pause",
|
|
21
|
+
"resume",
|
|
22
|
+
"complete",
|
|
23
|
+
"block",
|
|
24
|
+
"clear",
|
|
25
|
+
"continue",
|
|
26
|
+
]),
|
|
27
|
+
objective: z.string().optional(),
|
|
28
|
+
round: z.number().optional(),
|
|
29
|
+
maxRounds: z.number().optional(),
|
|
30
|
+
reason: z.string().optional(),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export type PlanModeCard = z.infer<typeof planModeCardSchema>;
|
|
34
|
+
export type PermissionPresetCard = z.infer<typeof permissionPresetCardSchema>;
|
|
35
|
+
export type GoalCard = z.infer<typeof goalCardSchema>;
|
|
36
|
+
|
|
37
|
+
/** Card kinds keyed by the `kind` field of the Paseo plugin timeline item. */
|
|
38
|
+
export const CARD_KINDS = {
|
|
39
|
+
planMode: "plan-mode",
|
|
40
|
+
permissionPreset: "permission-preset",
|
|
41
|
+
goal: "goal",
|
|
42
|
+
} as const;
|