Files
kandev/apps/web/components/task/session-tab-title.ts
T
João Salavisa 9381a6a9f7 feat(mcp): session spawning, sibling messaging, and renameable tabs (#1734)
* fix(scripts): launch dev-isolated backend via __backend subcommand

The kandev binary became a bundle launcher with the restart supervisor
(ADR 0019) and exits without KANDEV_BUNDLE_DIR when invoked bare; the
isolated dev instance must run the backend entrypoint directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(mcp): session spawning, sibling messaging, and renameable tabs

- spawn_session_kandev spawns an additional agent session on an existing
  task (default: the caller's own) via LaunchSession(IntentStart), with
  optional agent_profile_id (defaults to the spawner's profile) and a
  session name; the initial prompt carries spawner attribution and reply
  instructions.
- message_task_kandev accepts an optional session_id to target a specific
  session, enabling same-task sibling messaging. Explicit targets are
  pinned through dispatch so the idle-path primary-session re-resolution
  cannot reroute a sibling message back to its sender (regression-tested).
- task_sessions gains a name column (migration ordered after the
  agent_execution_id-drop rebuild), a session.rename WS action, and inline
  tab rename UI; custom names take precedence over derived agent/model
  labels across the tab, agents dropdown, mobile list, chat header, and
  dockview panel titles.
- Inter-session messages stamp sender_session_name into metadata so the
  sender badge shows which session sent the message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp): address review findings on session spawn/rename/messaging

- include name in task-session DTOs (list + full, no omitempty) so
  persisted tab names survive list hydration and clears propagate
- sanitize sender task/session ids embedded in <kandev-system> blocks
- reject cross-workspace spawn targets; require sender_task_id
- truncate session names by runes, not bytes
- distinguish not-found from internal errors in message target lookup
- guard TabRenameInput blur-commit after Enter/Escape resolution
- cap optimistic rename length client-side and toast on rename failure
- add tests: spawn launch-failure and cross-workspace paths, renameSession
  API client

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp): round-two review fixes for spawn/rename lint and hardening

- extract spawn validation/authorization helpers (cyclop/nestif limits)
- make stripSystemTag replace until stable to block nested tag evasion
- load session before rename so a durable rename can never skip its
  broadcast; surface lookup failures to the caller instead
- drop redundant sessionLauncher nil check (guarded registration)
- add maxLength to the rename input matching the server cap
- add tests: nested sanitizer evasion, spawn rename-failure tolerance

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 17:26:50 +01:00

52 lines
1.7 KiB
TypeScript

import {
displayModelName,
isModelConfigOption,
type DynamicConfigOption,
type ModelSelectorOption,
} from "@/components/model-config-selector";
type ResolveSessionTabTitleArgs = {
/** User-supplied session name; wins over every derived title when set. */
customName?: string | null;
agentLabel: string | null;
activeModelId: string | null;
currentModelId: string | null;
snapshotModel: string | null;
modelOptions: ModelSelectorOption[];
configOptions: DynamicConfigOption[];
};
function optionName(option: DynamicConfigOption, value: string): string {
return option.options?.find((item) => item.value === value)?.name ?? value;
}
function resolveModelTitle(
args: ResolveSessionTabTitleArgs,
modelId: string | null,
): string | null {
if (!modelId) return null;
const modelConfig = args.configOptions.find(isModelConfigOption);
let modelLabel = displayModelName(args.modelOptions, modelId);
if (modelConfig) {
// Use caller-supplied modelId, not modelConfig.currentValue, so live
// active/current model switches are reflected immediately in the tab title.
modelLabel = optionName(modelConfig, modelId);
}
const extras = args.configOptions
.filter((option) => !isModelConfigOption(option))
.map((option) => optionName(option, option.currentValue))
.filter(Boolean);
return [modelLabel, ...extras].join(" / ");
}
export function resolveSessionTabTitle(args: ResolveSessionTabTitleArgs): string | null {
if (args.customName) return args.customName;
const liveModelId = args.activeModelId || args.currentModelId;
return (
args.agentLabel ??
resolveModelTitle(args, liveModelId) ??
resolveModelTitle(args, args.snapshotModel)
);
}