Skip to content

Commit abaa6f9

Browse files
committed
fix(control-ui): create sessions for typed new
1 parent 99f1db3 commit abaa6f9

6 files changed

Lines changed: 91 additions & 55 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ Docs: https://docs.openclaw.ai
6464
- CLI/message: skip gateway-stop hooks for read-only `message read` and bound stop-hook shutdown for other message actions, so one-shot Discord reads cannot hang behind plugin lifecycle cleanup.
6565
- Plugins/web-provider: cache repeated bundled web search and web fetch provider registry loads by default while preserving explicit cache opt-outs. Supersedes #75992. Thanks @DmitryPogodaev.
6666
- Agents/sandbox: preserve existing workspace file modes when sandbox edits atomically replace files, so 0644 files do not collapse to 0600 after Write/Edit/apply_patch. Fixes #44077. Thanks @patosullivan.
67+
- Control UI/WebChat: route typed `/new` through the New Chat dashboard-session creation flow instead of `chat.send`, while keeping `/reset` as the explicit current-session reset. Fixes #69599. Thanks @WolvenRA.
6768
- Agents/models: keep legacy CLI runtime model refs such as `claude-cli/*` in the configured allowlist after canonical runtime migration, so cron `payload.model` overrides keep working. Fixes #75753. Thanks @RyanSandoval.
6869
- Codex/app-server: restart the shared Codex app-server client once when it closes during startup thread resume, preserving the existing thread binding instead of retrying `thread/start` on a closed client. Thanks @vincentkoc.
6970
- Gateway/watch: keep colored subsystem log prefixes in the managed tmux pane even when the parent shell exports `NO_COLOR`, while preserving explicit `FORCE_COLOR=0` opt-out. Thanks @vincentkoc.
@@ -647,7 +648,7 @@ Docs: https://docs.openclaw.ai
647648
- Outbound/security: strip known internal runtime scaffolding such as `<system-reminder>` and `<previous_response>` at the final channel delivery boundary and keep Discord output on targeted tag stripping, so degraded harness replies cannot leak those tags to users. Fixes #73595. Thanks @gabrielexito-stack and @martingarramon.
648649
- Security/Telegram: load Telegram security adapters in read-only audit/doctor, audit malformed Telegram DM `allowFrom` entries even when groups are disabled, and keep allowlist DM audits from counting stale pairing-store senders, so public/shared-DM risk checks stay accurate. Refs #73698. Thanks @xace1825.
649650
- Plugins: remove hidden manifest, provider-owner, bootstrap, and channel metadata caches so plugin installs, manifest edits, and bundled-root changes are visible on the next metadata read while keeping runtime/module loader caches for actual plugin code. Thanks @shakkernerd.
650-
- Control UI/WebChat: create a fresh dashboard session from the New Chat button instead of resetting the current transcript with `/new`, while keeping explicit `/new` reset behavior, preserving in-progress composer edits during delayed session creation or when creation cannot safely switch sessions, and showing clear retry feedback when creation is blocked, refreshing, or returns no new session. Carries forward #52042 and #52746. Thanks @bobashopcashier and @vincentkoc.
651+
- Control UI/WebChat: create a fresh dashboard session from the New Chat button instead of resetting the current transcript with `/new`, preserving in-progress composer edits during delayed session creation or when creation cannot safely switch sessions, and showing clear retry feedback when creation is blocked, refreshing, or returns no new session. Carries forward #52042 and #52746. Thanks @bobashopcashier and @vincentkoc.
651652
- CLI/plugins: use plugin metadata snapshots for install slot selection and add opt-in plugin lifecycle timing traces, so plugin install avoids runtime-loading the plugin registry for metadata-only decisions. Thanks @shakkernerd.
652653
- fix(plugins): restrict bundled plugin dir resolution to trusted package roots. (#73275) Thanks @pgondhi987.
653654
- fix(security): prevent workspace PATH injection via service env and trash helpers. (#73264) Thanks @pgondhi987.

docs/tools/slash-commands.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ Current source-of-truth:
125125
<AccordionGroup>
126126
<Accordion title="Sessions and runs">
127127
- `/new [model]` starts a new session; `/reset` is the reset alias.
128+
- Control UI intercepts typed `/new` to create and switch to a fresh dashboard session; typed `/reset` still runs the Gateway's in-place reset.
128129
- `/reset soft [message]` keeps the current transcript, drops reused CLI backend session ids, and reruns startup/system-prompt loading in-place.
129130
- `/compact [instructions]` compacts the session context. See [Compaction](/concepts/compaction).
130131
- `/stop` aborts the current run.

docs/web/control-ui.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ Imported themes are stored only in the current browser profile. They are not wri
157157
- During an active send and the final history refresh, the chat view keeps local optimistic user/assistant messages visible if `chat.history` briefly returns an older snapshot; the canonical transcript replaces those local messages once the Gateway history catches up.
158158
- `chat.inject` appends an assistant note to the session transcript and broadcasts a `chat` event for UI-only updates (no agent run, no channel delivery).
159159
- The chat header model and thinking pickers patch the active session immediately through `sessions.patch`; they are persistent session overrides, not one-turn-only send options.
160+
- Typing `/new` in the Control UI creates and switches to the same fresh dashboard session as New Chat. Typing `/reset` keeps the Gateway's explicit in-place reset for the current session.
160161
- The chat model picker requests the Gateway's configured model view. If `agents.defaults.models` is present, that allowlist drives the picker. Otherwise the picker shows explicit `models.providers.*.models` entries plus providers with usable auth. The full catalog stays available through the debug `models.list` RPC with `view: "all"`.
161162
- When fresh Gateway session usage reports show high context pressure, the chat composer area shows a context notice and, at recommended compaction levels, a compact button that runs the normal session compaction path. Stale token snapshots are hidden until the Gateway reports fresh usage again.
162163

ui/src/ui/app-chat.test.ts

Lines changed: 70 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -469,67 +469,96 @@ describe("handleSendChat", () => {
469469
expect(host.refreshSessionsAfterChat.size).toBe(0);
470470
});
471471

472-
it("sends button-triggered /new resets after confirmation", async () => {
472+
it("runs the fresh-session action for confirmed /new overrides", async () => {
473473
const confirm = vi.fn(() => true);
474474
vi.stubGlobal("confirm", confirm);
475475
const request = vi.fn(async (method: string) => {
476-
if (method === "chat.send") {
477-
return { status: "started" };
478-
}
479476
throw new Error(`Unexpected request: ${method}`);
480477
});
478+
const onSlashAction = vi.fn();
481479
const host = makeHost({
482480
client: { request } as unknown as ChatHost["client"],
483481
chatMessage: "restore me",
484482
sessionKey: "agent:main",
483+
onSlashAction,
485484
});
486485

487486
await handleSendChat(host, "/new", { confirmReset: true, restoreDraft: true });
488487

489488
expect(confirm).toHaveBeenCalledTimes(1);
489+
expect(request).not.toHaveBeenCalled();
490+
expect(onSlashAction).toHaveBeenCalledWith("new-session");
491+
expect(host.chatMessage).toBe("restore me");
492+
expect(host.refreshSessionsAfterChat.size).toBe(0);
493+
});
494+
495+
it("routes typed /new through the fresh-session action without confirmation", async () => {
496+
const confirm = vi.fn(() => false);
497+
vi.stubGlobal("confirm", confirm);
498+
const request = vi.fn(async (method: string) => {
499+
throw new Error(`Unexpected request: ${method}`);
500+
});
501+
const onSlashAction = vi.fn();
502+
const host = makeHost({
503+
client: { request } as unknown as ChatHost["client"],
504+
chatMessage: "/new",
505+
sessionKey: "agent:main",
506+
onSlashAction,
507+
});
508+
509+
await handleSendChat(host);
510+
511+
expect(confirm).not.toHaveBeenCalled();
512+
expect(request).not.toHaveBeenCalled();
513+
expect(onSlashAction).toHaveBeenCalledWith("new-session");
514+
expect(host.chatMessage).toBe("");
515+
});
516+
517+
it("does not queue typed /new behind an active run", async () => {
518+
const onSlashAction = vi.fn();
519+
const host = makeHost({
520+
chatMessage: "/new",
521+
chatRunId: "run-main",
522+
chatStream: "Working...",
523+
onSlashAction,
524+
});
525+
526+
await handleSendChat(host);
527+
528+
expect(onSlashAction).toHaveBeenCalledWith("new-session");
529+
expect(host.chatQueue).toEqual([]);
530+
expect(host.chatRunId).toBe("run-main");
531+
expect(host.chatStream).toBe("Working...");
532+
expect(host.chatMessage).toBe("");
533+
});
534+
535+
it("preserves typed /reset command dispatch without confirmation", async () => {
536+
const confirm = vi.fn(() => false);
537+
vi.stubGlobal("confirm", confirm);
538+
const request = vi.fn(async (method: string) => {
539+
if (method === "chat.send") {
540+
return { status: "started" };
541+
}
542+
throw new Error(`Unexpected request: ${method}`);
543+
});
544+
const host = makeHost({
545+
client: { request } as unknown as ChatHost["client"],
546+
chatMessage: "/reset",
547+
sessionKey: "agent:main",
548+
});
549+
550+
await handleSendChat(host);
551+
552+
expect(confirm).not.toHaveBeenCalled();
490553
expect(request).toHaveBeenCalledWith(
491554
"chat.send",
492555
expect.objectContaining({
493556
sessionKey: "agent:main",
494-
message: "/new",
495-
deliver: false,
496-
idempotencyKey: expect.any(String),
557+
message: "/reset",
497558
}),
498559
);
499-
expect(host.chatMessage).toBe("restore me");
500-
expect(host.refreshSessionsAfterChat).toContain(host.chatRunId);
501-
});
502-
503-
it.each(["/new", "/reset"])(
504-
"preserves typed %s command dispatch without confirmation",
505-
async (command) => {
506-
const confirm = vi.fn(() => false);
507-
vi.stubGlobal("confirm", confirm);
508-
const request = vi.fn(async (method: string) => {
509-
if (method === "chat.send") {
510-
return { status: "started" };
511-
}
512-
throw new Error(`Unexpected request: ${method}`);
513-
});
514-
const host = makeHost({
515-
client: { request } as unknown as ChatHost["client"],
516-
chatMessage: command,
517-
sessionKey: "agent:main",
518-
});
519-
520-
await handleSendChat(host);
521-
522-
expect(confirm).not.toHaveBeenCalled();
523-
expect(request).toHaveBeenCalledWith(
524-
"chat.send",
525-
expect.objectContaining({
526-
sessionKey: "agent:main",
527-
message: command,
528-
}),
529-
);
530-
expect(host.chatMessage).toBe("");
531-
},
532-
);
560+
expect(host.chatMessage).toBe("");
561+
});
533562

534563
it("keeps slash-command model changes in sync with the chat header cache", async () => {
535564
vi.stubGlobal(

ui/src/ui/app-chat.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export type ChatHost = ChatInputHistoryState & {
6969
pendingAbort?: { runId: string; sessionKey: string } | null;
7070
chatSubmitGuards?: Map<string, Promise<void>>;
7171
/** Callback for slash-command side effects that need app-level access. */
72-
onSlashAction?: (action: string) => void;
72+
onSlashAction?: (action: string) => void | Promise<void>;
7373
};
7474

7575
export type ChatSendOptions = {
@@ -527,7 +527,7 @@ export async function handleSendChat(
527527
}
528528

529529
function shouldQueueLocalSlashCommand(name: string): boolean {
530-
return !["stop", "focus", "export-session", "steer", "redirect"].includes(name);
530+
return !["stop", "focus", "export-session", "steer", "redirect", "new"].includes(name);
531531
}
532532

533533
// ── Slash Command Dispatch ──
@@ -543,11 +543,11 @@ async function dispatchSlashCommand(
543543
await handleAbortChat(host);
544544
return;
545545
case "new":
546-
await sendChatMessageNow(host, "/new", {
547-
refreshSessions: true,
548-
previousDraft: sendOpts?.previousDraft,
549-
restoreDraft: sendOpts?.restoreDraft,
550-
});
546+
if (!host.onSlashAction) {
547+
host.lastError = "New Chat is unavailable.";
548+
return;
549+
}
550+
await host.onSlashAction("new-session");
551551
return;
552552
case "reset":
553553
await sendChatMessageNow(host, "/reset", {
@@ -560,10 +560,10 @@ async function dispatchSlashCommand(
560560
await clearChatHistory(host);
561561
return;
562562
case "focus":
563-
host.onSlashAction?.("toggle-focus");
563+
await host.onSlashAction?.("toggle-focus");
564564
return;
565565
case "export-session":
566-
host.onSlashAction?.("export");
566+
await host.onSlashAction?.("export");
567567
return;
568568
}
569569

@@ -596,7 +596,7 @@ async function dispatchSlashCommand(
596596
...host.chatModelOverrides,
597597
[targetSessionKey]: result.sessionPatch.modelOverride ?? null,
598598
};
599-
host.onSlashAction?.("refresh-tools-effective");
599+
await host.onSlashAction?.("refresh-tools-effective");
600600
}
601601

602602
if (result.action === "refresh") {

ui/src/ui/app.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
handleFirstUpdated,
3535
handleUpdated,
3636
} from "./app-lifecycle.ts";
37+
import { createChatSession as createChatSessionInternal } from "./app-render.helpers.ts";
3738
import { renderApp } from "./app-render.ts";
3839
import {
3940
exportLogs as exportLogsInternal,
@@ -225,7 +226,7 @@ export class OpenClawApp extends LitElement {
225226
private chatMobileControlsTrigger: HTMLElement | null = null;
226227
@state() navDrawerOpen = false;
227228

228-
onSlashAction?: (action: string) => void;
229+
onSlashAction?: (action: string) => void | Promise<void>;
229230
chatLocalInputHistoryBySession: Record<string, Array<{ text: string; ts: number }>> = {};
230231
chatInputHistorySessionKey: string | null = null;
231232
chatInputHistoryItems: string[] | null = null;
@@ -605,8 +606,11 @@ export class OpenClawApp extends LitElement {
605606

606607
connectedCallback() {
607608
super.connectedCallback();
608-
this.onSlashAction = (action: string) => {
609+
this.onSlashAction = async (action: string) => {
609610
switch (action) {
611+
case "new-session":
612+
await createChatSessionInternal(this as unknown as AppViewState);
613+
break;
610614
case "toggle-focus":
611615
this.applySettings({
612616
...this.settings,
@@ -617,7 +621,7 @@ export class OpenClawApp extends LitElement {
617621
exportChatMarkdown(this.chatMessages, this.assistantName);
618622
break;
619623
case "refresh-tools-effective": {
620-
void refreshVisibleToolsEffectiveForCurrentSessionInternal(this);
624+
await refreshVisibleToolsEffectiveForCurrentSessionInternal(this);
621625
break;
622626
}
623627
}

0 commit comments

Comments
 (0)