Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/mobile/src/features/threads/DraftThreadHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export function DraftThreadHeader(props: {
canOperateTerminal: false,
canOpenFiles: canReadFiles && props.cwd !== null,
projectScripts: [],
canRunPrompt: false,
terminalSessions: [],
onOpenFilesInspector: openFiles,
onOpenGitInspector: requiresThread,
Expand Down
17 changes: 11 additions & 6 deletions apps/mobile/src/features/threads/ThreadGitControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ type ThreadGitControlsProps = ThreadGitMenuProps & {
};
readonly canOpenTerminal: boolean;
readonly canOperateTerminal: boolean;
readonly canRunPrompt: boolean;
readonly canOpenFiles: boolean;
readonly projectScripts: ReadonlyArray<ProjectScript>;
readonly terminalSessions: ReadonlyArray<TerminalMenuSession>;
Expand Down Expand Up @@ -281,15 +282,15 @@ function useThreadGitHeaderActionItems(props: ThreadGitControlsProps): ThreadGit
return {
terminal: {
accessibilityLabel: "Open terminal",
disabled: !props.canOpenTerminal,
disabled: !props.canOpenTerminal && !props.canRunPrompt,
icon: { name: "terminal", type: "sfSymbol" },
identifier: "thread-right-terminal",
label: "Terminal",
menu: {
items: [
...props.projectScripts.map((script) => ({
description: script.command,
disabled: !props.canOperateTerminal,
description: script.type === "prompt" ? script.prompt : script.command,
disabled: script.type === "prompt" ? !props.canRunPrompt : !props.canOperateTerminal,
icon: { name: projectScriptMenuIcon(script.icon), type: "sfSymbol" as const },
label: projectScriptMenuLabel(script),
onPress: () => void props.onRunProjectScript(script),
Expand All @@ -308,6 +309,7 @@ function useThreadGitHeaderActionItems(props: ThreadGitControlsProps): ThreadGit
]
: []),
...props.terminalSessions.map((session) => ({
disabled: !props.canOpenTerminal,
description: [
getTerminalStatusLabel({
status: session.status,
Expand Down Expand Up @@ -449,17 +451,19 @@ export function ThreadGitControls(props: ThreadGitControlsProps) {
<Toolbar.Menu
accessibilityLabel="Open terminal"
icon="terminal"
disabled={!props.canOpenTerminal}
disabled={!props.canOpenTerminal && !props.canRunPrompt}
Comment thread
JamesOBrien2 marked this conversation as resolved.
Resolved
separateBackground
>
{props.projectScripts.length > 0 ? (
props.projectScripts.map((script) => (
<Toolbar.MenuAction
key={script.id}
icon={projectScriptMenuIcon(script.icon)}
disabled={!props.canOperateTerminal}
disabled={
script.type === "prompt" ? !props.canRunPrompt : !props.canOperateTerminal
}
onPress={() => void props.onRunProjectScript(script)}
subtitle={script.command}
subtitle={script.type === "prompt" ? script.prompt : script.command}
>
<Toolbar.Label>{projectScriptMenuLabel(script)}</Toolbar.Label>
</Toolbar.MenuAction>
Expand All @@ -478,6 +482,7 @@ export function ThreadGitControls(props: ThreadGitControlsProps) {
<Toolbar.MenuAction
key={session.terminalId}
icon="terminal"
disabled={!props.canOpenTerminal}
onPress={() => props.onOpenTerminal(session.terminalId)}
subtitle={[
getTerminalStatusLabel({
Expand Down
28 changes: 28 additions & 0 deletions apps/mobile/src/features/threads/ThreadRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import { useSelectedThreadGitState } from "../../state/use-selected-thread-git-s
import { useSelectedThreadRequests } from "../../state/use-selected-thread-requests";
import { useSelectedThreadWorktree } from "../../state/use-selected-thread-worktree";
import { useThreadComposerState } from "../../state/use-thread-composer-state";
import { updateComposerDraftSettings } from "../../state/use-composer-drafts";
import { resolveMergeBackTargetThreadId } from "@t3tools/client-runtime/state/thread-relationships";
import { resolveLatestMergeBackRun } from "@t3tools/client-runtime/state/thread-workflows";
import { threadEnvironment, useThreadFamily } from "../../state/threads";
Expand Down Expand Up @@ -692,7 +693,33 @@ function ThreadRouteContent(
});
};

const runPrompt = useAtomCommand(threadEnvironment.runProjectPrompt, "run prompt action");
const handleRunProjectScript = async (script: ProjectScript) => {
if (script.type === "prompt") {
if (
!selectedThread ||
!readEnvironmentScope(selectedThread.environmentId, AuthOrchestrationOperateScope)
)
return;
const result = await runPrompt({
environmentId: selectedThread.environmentId,
input: {
threadId: selectedThread.id,
action: script,
creationSource: "mobile",
...(composer.modelSelection ? { modelSelection: composer.modelSelection } : {}),
...(composer.runtimeMode ? { runtimeMode: composer.runtimeMode } : {}),
...(composer.interactionMode ? { interactionMode: composer.interactionMode } : {}),
},
});
if (result._tag === "Success" && script.modelSelection) {
updateComposerDraftSettings(
scopedThreadKey(selectedThread.environmentId, selectedThread.id),
{ modelSelection: script.modelSelection },
);
}
return;
}
terminalDebugLog("project-script:press", {
scriptId: script.id,
command: script.command,
Expand Down Expand Up @@ -783,6 +810,7 @@ function ThreadRouteContent(
canOpenTerminal:
Boolean(selectedThreadProject?.workspaceRoot) && (canReadTerminal || canOperateTerminal),
canOperateTerminal,
canRunPrompt: canOperateThread,
canOpenFiles: Boolean(selectedThreadProject?.workspaceRoot),
projectScripts: selectedThreadProject
? resolveProjectScripts(
Expand Down
23 changes: 23 additions & 0 deletions apps/server/src/orchestration-v2/CommandPolicy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { assert, it } from "@effect/vitest";
import {
CommandId,
MessageId,
type ModelSelection,
type OrchestrationV2ProviderCapabilities,
type OrchestrationV2ThreadProjection,
ProviderInstanceId,
Expand Down Expand Up @@ -200,6 +201,28 @@ it.each(["/compact", " /LOGOUT "])("queues a steer sent while %s runs", (text) =
const layer = it.layer(CommandPolicy.layer);

layer("CommandPolicyV2", (it) => {
it.effect("starts an idle queued prompt with the thread's arbitrary model and options", () =>
Effect.gen(function* () {
const policy = yield* CommandPolicy.CommandPolicyV2;
const modelSelection: ModelSelection = {
instanceId: ProviderInstanceId.make("custom-runtime"),
model: "deepseek/deepseek-v4.1-flash:free",
options: [{ id: "reasoningEffort", value: "high" }],
};
const projection = {
...dispatchProjection(),
thread: { id: threadId, modelSelection },
} as OrchestrationV2ThreadProjection;
const decision = yield* policy.decideMessageDispatch({
commandId,
projection,
requestedMode: { type: "queue_after_active" },
capabilities: baseCapabilities,
});
assert.deepEqual(decision, { type: "start_run", modelSelection });
}),
);

it.effect("falls back to portable context when Cursor cannot fork natively", () =>
Effect.gen(function* () {
const policy = yield* CommandPolicy.CommandPolicyV2;
Expand Down
82 changes: 82 additions & 0 deletions apps/server/src/serverSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1721,6 +1721,88 @@ it.layer(NodeServices.layer)("server settings", (it) => {
},
);

it.effect(
"persists prompt actions beside legacy shells and preserves unrelated settings on later saves",
() =>
Effect.gen(function* () {
const service = yield* ServerSettingsModule.ServerSettingsService;
const config = yield* ServerConfig.ServerConfig;
const fs = yield* FileSystem.FileSystem;
const projectId = ProjectId.make("prompt-project");
const shell: ProjectScript = {
id: "test",
name: "Test",
command: "npm test",
icon: "test",
runOnWorktreeCreate: true,
async: false,
runOnSettle: true,
};
const prompt: ProjectScript = {
id: "status",
name: "Status",
type: "prompt",
prompt: "Read project instructions.\nReport completed results without changes.",
icon: "play",
runOnWorktreeCreate: false,
};
yield* service.updateSettings({
projectSettingsOverrides: {
[projectId]: { defaultAutoPull: true, defaultProjectScripts: [shell, prompt] },
},
});
const saved = yield* decodeServerSettingsJson(
yield* fs.readFileString(config.settingsPath),
);
assert.deepEqual(saved.projectSettingsOverrides[projectId], {
defaultAutoPull: true,
defaultProjectScripts: [shell, prompt],
});
const reloaded = yield* Effect.gen(function* () {
const fresh = yield* ServerSettingsModule.ServerSettingsService;
return yield* fresh.getSettings;
}).pipe(
Effect.provide(
Layer.fresh(ServerSettingsModule.layer).pipe(Layer.provide(ServerSecretStore.layer)),
),
);
assert.deepEqual(
reloaded.projectSettingsOverrides[projectId],
saved.projectSettingsOverrides[projectId],
);
yield* service.updateSettings({
projectSettingsOverrides: {
[projectId]: {
...saved.projectSettingsOverrides[projectId],
defaultProjectScripts: [shell, { ...prompt, prompt: "Inspect new results." }],
},
},
});
const edited = yield* decodeServerSettingsJson(
yield* fs.readFileString(config.settingsPath),
);
assert.deepEqual(edited.projectSettingsOverrides[projectId]?.defaultProjectScripts, [
shell,
{ ...prompt, prompt: "Inspect new results." },
]);
yield* service.updateSettings({
projectSettingsOverrides: {
[projectId]: {
...edited.projectSettingsOverrides[projectId],
defaultProjectScripts: [shell],
},
},
});
const deleted = yield* decodeServerSettingsJson(
yield* fs.readFileString(config.settingsPath),
);
assert.deepEqual(deleted.projectSettingsOverrides[projectId], {
defaultAutoPull: true,
defaultProjectScripts: [shell],
});
}).pipe(Effect.provide(layerServerSettings())),
);

it.effect("folds legacy project overrides into projectSettingsOverrides once", () =>
Effect.gen(function* () {
const serverConfig = yield* ServerConfig.ServerConfig;
Expand Down
10 changes: 8 additions & 2 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1823,8 +1823,12 @@ export default function ChatView(props: ChatViewProps) {
composerRef,
terminalUiState,
activeThread,
modelSelection: composerSelectedModelSelection ?? activeThread?.modelSelection,
runtimeMode,
interactionMode,
activeThreadId,
activeThreadRef,
isServerThread,
activeProject,
activeRightPanelSurface,
runningTerminalIds,
Expand Down Expand Up @@ -2675,9 +2679,11 @@ export default function ChatView(props: ChatViewProps) {
},
fallback: (command) => {
const scriptId = projectScriptIdFromCommand(command);
if (!scriptId || !activeProject || !canOperateTerminal) return undefined;
if (!scriptId || !activeProject) return undefined;
const script = activeProjectScripts.find((entry) => entry.id === scriptId);
return script ? () => void runProjectScriptStable(script) : undefined;
if (!script || (script.type === "prompt" ? !canOperateThread : !canOperateTerminal))
return undefined;
return () => void runProjectScriptStable(script);
},
});

Expand Down
6 changes: 5 additions & 1 deletion apps/web/src/components/ProjectScriptsControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ThreadDetailsControl } from "./chat/ThreadDetailsControl";
import {
AuthSettingsWriteScope,
type EnvironmentId,
type ProjectId,
type ProjectScript,
type T3ProjectFileScript,
} from "@t3tools/contracts";
Expand Down Expand Up @@ -63,6 +64,7 @@ interface ProjectScriptsControlProps {
presentation?: "toolbar" | "menu";
onRequestMenuClose?: () => void;
environmentId: EnvironmentId;
projectId: ProjectId;
scripts: ReadonlyArray<ProjectScript>;
fileScripts?: ReadonlyArray<T3ProjectFileScript>;
preferredScriptId?: string | null;
Expand All @@ -80,6 +82,7 @@ export default function ProjectScriptsControl({
presentation = "toolbar",
onRequestMenuClose,
environmentId,
projectId,
scripts,
fileScripts = NO_FILE_SCRIPTS,
preferredScriptId = null,
Expand Down Expand Up @@ -111,7 +114,7 @@ export default function ProjectScriptsControl({
(fileScript) =>
!scripts.some(
(script) =>
script.command === fileScript.command ||
(script.type !== "prompt" && script.command === fileScript.command) ||
script.name.toLowerCase() === fileScript.name.toLowerCase(),
),
);
Expand Down Expand Up @@ -450,6 +453,7 @@ export default function ProjectScriptsControl({

<ProjectScriptEditorDialog
environmentId={environmentId}
projectId={projectId}
request={editorRequest}
scripts={scripts}
onSubmit={submitScript}
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/chat/ThreadDetailsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export function ThreadDetailsPanel(props: ThreadDetailsPanelProps) {
{props.activeProjectScripts ? (
<ProjectScriptsControl
environmentId={props.environmentId}
projectId={props.projectId}
displayMode="panel"
scripts={props.activeProjectScripts}
fileScripts={fileScripts}
Expand Down
Loading
Loading