Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/fix-web-vite-ws-proxy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix WebSocket connections through the web dev and preview proxies.
5 changes: 5 additions & 0 deletions .changeset/web-session-permission-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Keep web runtime modes scoped to the active session instead of applying one global mode across sessions.
1 change: 1 addition & 0 deletions apps/kimi-web/src/api/daemon/agentEventProjector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,7 @@ export function createAgentProjector(): AgentProjector {
// for a "make a plan" prompt). Carry it so the composer's plan toggle
// reflects the agent's real state, not just the user's manual choice.
planMode: p?.planMode === true ? true : p?.planMode === false ? false : undefined,
permissionMode: typeof p?.permission === 'string' ? p.permission : undefined,
});
break;
}
Expand Down
2 changes: 1 addition & 1 deletion apps/kimi-web/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ export type AppEvent =
| { type: 'sessionDeleted'; sessionId: string }
| { type: 'sessionStatusChanged'; sessionId: string; status: AppSessionStatus; previousStatus: AppSessionStatus; currentPromptId?: string }
| { type: 'sessionMetaUpdated'; sessionId: string; title?: string; lastPrompt?: string }
| { type: 'sessionUsageUpdated'; sessionId: string; usage: AppSessionUsage; model?: string; swarmMode?: boolean; planMode?: boolean }
| { type: 'sessionUsageUpdated'; sessionId: string; usage: AppSessionUsage; model?: string; swarmMode?: boolean; planMode?: boolean; permissionMode?: string }
| { type: 'historyCompacted'; sessionId: string; beforeSeq: number; reason: string; summaryMessageId?: string }
| { type: 'compactionStarted'; sessionId: string; trigger: 'manual' | 'auto'; instruction?: string }
| { type: 'compactionCompleted'; sessionId: string; tokensBefore?: number; tokensAfter?: number; summary?: string }
Expand Down
133 changes: 101 additions & 32 deletions apps/kimi-web/src/composables/client/useWorkspaceState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,13 @@ export interface UseWorkspaceStateDeps {
status: ComputedRef<ConversationStatus>;
workspaceIdForSession: (s: { workspaceId?: string; cwd: string }) => string;
savePermissionToStorage: (mode: PermissionMode) => void;
savePlanModeToStorage: (v: boolean) => void;
saveSwarmModeToStorage: (v: boolean) => void;
saveGoalModeToStorage: (v: boolean) => void;
savePermissionModeToStorage: () => void;
/** Persist the current per-session mode maps (read off rawState). */
savePlanModeToStorage: () => void;
saveSwarmModeToStorage: () => void;
saveGoalModeToStorage: () => void;
/** Staged mode toggles for the not-yet-created draft session. */
draftModes: { planMode: boolean; swarmMode: boolean; goalMode: boolean };
saveUnread: (changes: Record<string, boolean>) => void;
saveActiveWorkspaceToStorage: (id: string) => void;
saveHiddenWorkspacesToStorage: (roots: string[]) => void;
Expand Down Expand Up @@ -133,9 +137,11 @@ export function useWorkspaceState(rawState: ExtendedState, deps: UseWorkspaceSta
status,
workspaceIdForSession,
savePermissionToStorage,
savePermissionModeToStorage,
savePlanModeToStorage,
saveSwarmModeToStorage,
saveGoalModeToStorage,
draftModes,
saveUnread,
saveActiveWorkspaceToStorage,
saveHiddenWorkspacesToStorage,
Expand Down Expand Up @@ -654,12 +660,45 @@ export function useWorkspaceState(rawState: ExtendedState, deps: UseWorkspaceSta
// then submitPromptInternal adds the user turn synchronously (no await in
// between), so the view goes loading → message with no empty-composer frame.
await selectSession(session.id);
// Carry any mode toggles the user staged in the empty composer into the
// newly-created session, so the first prompt honors them. Write them to
// this session's per-session maps by id (not via the activeSessionId-based
// setters): if the user switches to another session while selectSession is
// awaiting the snapshot, the setters would otherwise read the then-current
// activeSessionId and pollute that session while this one loses the modes.
const sid = session.id;
rawState.permissionModeBySession = {
...rawState.permissionModeBySession,
[sid]: rawState.permission,
};
savePermissionModeToStorage();
if (draftModes.planMode) {
rawState.planModeBySession = { ...rawState.planModeBySession, [sid]: true };
savePlanModeToStorage();
}
if (draftModes.swarmMode) {
rawState.swarmModeBySession = { ...rawState.swarmModeBySession, [sid]: true };
saveSwarmModeToStorage();
}
if (draftModes.goalMode) {
rawState.goalModeBySession = { ...rawState.goalModeBySession, [sid]: true };
saveGoalModeToStorage();
}
draftModes.planMode = false;
draftModes.swarmMode = false;
draftModes.goalMode = false;
await submitPromptInternal(session.id, text, attachments);
} catch (err) {
pushOperationFailure('startSessionAndSendPrompt', err);
}
}

function permissionForSession(sessionId: string | null | undefined): PermissionMode {
return sessionId
? (rawState.permissionModeBySession[sessionId] ?? rawState.permission)
: rawState.permission;
}

/**
* Add a workspace by folder path. Tries the daemon registry; on failure (or in
* fallback mode) creates a locally-derived workspace from the path and
Expand Down Expand Up @@ -893,8 +932,11 @@ export function useWorkspaceState(rawState: ExtendedState, deps: UseWorkspaceSta
(promptSession?.model && promptSession.model.length > 0
? promptSession.model
: rawState.defaultModel) ?? undefined;
const planMode = rawState.planModeBySession[sid] ?? false;
const swarmMode = rawState.swarmModeBySession[sid] ?? false;
const goalMode = rawState.goalModeBySession[sid] ?? false;

if (rawState.goalMode && text) {
if (goalMode && text) {
try {
await api.updateSession(sid, { goalObjective: text.trim() });
} catch (err) {
Expand All @@ -912,14 +954,14 @@ export function useWorkspaceState(rawState: ExtendedState, deps: UseWorkspaceSta
content,
model,
thinking: rawState.thinking,
permissionMode: rawState.permission,
planMode: rawState.planMode,
swarmMode: rawState.swarmMode,
permissionMode: permissionForSession(sid),
planMode,
swarmMode,
});

if (rawState.goalMode) {
rawState.goalMode = false;
saveGoalModeToStorage(false);
if (goalMode) {
rawState.goalModeBySession = { ...rawState.goalModeBySession, [sid]: false };
saveGoalModeToStorage();
}

// Authoritative prompt_id for :abort — race-free (the projector binding can
Expand Down Expand Up @@ -1045,9 +1087,9 @@ export function useWorkspaceState(rawState: ExtendedState, deps: UseWorkspaceSta
content,
model,
thinking: rawState.thinking,
permissionMode: rawState.permission,
planMode: rawState.planMode,
swarmMode: rawState.swarmMode,
permissionMode: permissionForSession(sid),
planMode: rawState.planModeBySession[sid] ?? false,
swarmMode: rawState.swarmModeBySession[sid] ?? false,
});

// Stamp the real prompt_id onto the optimistic echo. Unlike a normal send,
Expand Down Expand Up @@ -1243,50 +1285,71 @@ export function useWorkspaceState(rawState: ExtendedState, deps: UseWorkspaceSta

/** Persist and apply plan mode (pushed to the session profile + sent per-prompt). */
function setPlanMode(on: boolean): void {
rawState.planMode = on;
savePlanModeToStorage(on);
persistSessionProfile({ planMode: on });
const sid = rawState.activeSessionId;
if (sid) {
rawState.planModeBySession = { ...rawState.planModeBySession, [sid]: on };
savePlanModeToStorage();
persistSessionProfile({ planMode: on });
} else {
draftModes.planMode = on;
}
}

/** Flip plan mode on/off. */
function togglePlanMode(): void {
setPlanMode(!rawState.planMode);
const sid = rawState.activeSessionId;
const current = sid ? (rawState.planModeBySession[sid] ?? false) : draftModes.planMode;
setPlanMode(!current);
}

/** Persist and apply swarm mode (pushed to the session profile + sent per-prompt). */
function setSwarmMode(on: boolean): void {
rawState.swarmMode = on;
saveSwarmModeToStorage(on);
persistSessionProfile({ swarmMode: on });
const sid = rawState.activeSessionId;
if (sid) {
rawState.swarmModeBySession = { ...rawState.swarmModeBySession, [sid]: on };
saveSwarmModeToStorage();
persistSessionProfile({ swarmMode: on });
} else {
draftModes.swarmMode = on;
}
}

/** Flip swarm mode on/off. In manual permission mode, ask before enabling. */
function toggleSwarmMode(): void {
const on = !rawState.swarmMode;
if (on && rawState.permission === 'manual') {
const ok = confirm('Enable swarm mode? The agent will run multiple sub agents in parallel.');
async function toggleSwarmMode(): Promise<void> {
const sid = rawState.activeSessionId;
const current = sid ? (rawState.swarmModeBySession[sid] ?? false) : draftModes.swarmMode;
const on = !current;
if (on && permissionForSession(sid) === 'manual') {
const ok = globalThis.confirm('Enable swarm mode? The agent will run multiple sub agents in parallel.');
if (!ok) return;
}
setSwarmMode(on);
}

/** Persist goal mode locally. Unlike plan/swarm, this is a one-shot flag consumed on send. */
function setGoalMode(on: boolean): void {
rawState.goalMode = on;
saveGoalModeToStorage(on);
const sid = rawState.activeSessionId;
if (sid) {
rawState.goalModeBySession = { ...rawState.goalModeBySession, [sid]: on };
saveGoalModeToStorage();
} else {
draftModes.goalMode = on;
}
}

/** Flip goal mode on/off. */
function toggleGoalMode(): void {
setGoalMode(!rawState.goalMode);
const sid = rawState.activeSessionId;
const current = sid ? (rawState.goalModeBySession[sid] ?? false) : draftModes.goalMode;
setGoalMode(!current);
}

/** Create a goal by sending its objective to the session profile, then submit it as a prompt. */
async function createGoal(objective: string): Promise<void> {
const trimmed = objective.trim();
if (!trimmed) return;
if (rawState.permission === 'manual') {
const ok = confirm(`Start goal: "${trimmed}"? The agent will run autonomously toward this objective.`);
if (permissionForSession(rawState.activeSessionId) === 'manual') {
const ok = globalThis.confirm(`Start goal: "${trimmed}"? The agent will run autonomously toward this objective.`);
if (!ok) return;
}
const sid = rawState.activeSessionId;
Expand Down Expand Up @@ -1314,9 +1377,15 @@ export function useWorkspaceState(rawState: ExtendedState, deps: UseWorkspaceSta
* the daemon (auto/yolo are resolved server-side), so any pending approvals
* are left for the user to answer explicitly. */
function setPermission(mode: PermissionMode): void {
rawState.permission = mode;
savePermissionToStorage(mode);
persistSessionProfile({ permissionMode: mode });
const sid = rawState.activeSessionId;
if (sid) {
rawState.permissionModeBySession = { ...rawState.permissionModeBySession, [sid]: mode };
savePermissionModeToStorage();
persistSessionProfile({ permissionMode: mode });
} else {
rawState.permission = mode;
savePermissionToStorage(mode);
}
}

/** Dismiss a warning by index */
Expand Down
Loading