export type Attachment =
  | { type: 'webpage'; url: string; title: string; content: string; attachmentId: string; loading?: boolean }
  | { type: 'image'; filename: string; dataUrl: string; attachmentId: string; loading?: boolean };

export type MessageCost = {
  inputTokens: number;
  outputTokens: number;
  totalUSD: number;
};

export type ModelOption = {
  id: string;
  name: string;
  provider: string;
  contextWindow: number;
  maxTokens: number;
  reasoning?: boolean;
  images?: boolean;
};

export type ChatMessage =
  | { role: 'user'; text: string; images?: { filename: string; dataUrl: string }[] }
  | { role: 'assistant'; id?: string; text: string }
  | { role: 'system'; text: string }
  | {
      role: 'tool';
      toolName: string;
      toolInput: Record<string, unknown>;
      toolCallId: string;
      output?: string;
      isError?: boolean;
      /**
       * For a `Task` call: everything the subagent said and did, in order. Kept nested rather than
       * appended to the transcript because a subagent's work is not the conversation — flattened, its
       * prose read as the agent you are talking to having said it, and its file edits looked like yours.
       */
      children?: ChatMessage[];
    }
  | { role: 'result'; cost: MessageCost }
  | { role: 'error'; text: string }
  // You pressed stop. Deliberately not an `error` — the turn did what you told it to, and dressing that
  // in red destructive chrome (which is what it looked like) reads as "something went wrong".
  | { role: 'interrupted' }
  // The agent process went away mid-turn. Carries the prompt that was lost with it, so the row can offer
  // to send it again — the conversation itself is on disk and the next turn resumes it.
  | { role: 'cutoff'; prompt: string }
  // Where a `/clear` fell inside a resumed conversation. The server splices the parts of a chain into
  // one transcript (see `loadChainTranscript`), and this is the seam — deliberately visible, because
  // your history runs straight through it and the agent's context does not. `sessionId` is the part
  // that ends here.
  | { role: 'divider'; sessionId: string }
  | {
      // A background task (run_in_background / Monitor). One row for its whole life: it appears pending
      // and resolves in place, rather than as two unrelated bubbles minutes apart.
      role: 'task';
      taskId: string;
      description: string;
      taskType?: string;
      status?: 'completed' | 'failed' | 'stopped';
      summary?: string;
    }
  | {
      // The harness rewriting its own context, start to finish, in one row — the same shape as `task`,
      // for the same reason: it appears the moment compaction begins and resolves in place when it ends.
      // Compaction is the one thing that produces no output at all while it runs, so before this row a
      // long one looked exactly like a hung turn.
      role: 'compact';
      trigger: 'manual' | 'auto';
      preTokens?: number;
      durationMs?: number;
      done?: boolean;
    };

export type RunningTask = { taskId: string; description: string; taskType?: string };

export type TaskInfo = {
  taskName: string;
  taskDirName: string;
  entryName: string;
  entryType: 'file' | 'directory';
};

export type ServerMessage =
  | { type: 'session:init'; sessionId: string; model: string; cwd: string; context?: string; contextId?: string }
  | { type: 'assistant:text'; text: string; parentToolUseId?: string }
  | { type: 'assistant:delta'; text: string; parentToolUseId?: string }
  | {
      type: 'tool:start';
      toolCallId: string;
      toolName: string;
      toolInput: Record<string, unknown>;
      parentToolUseId?: string;
    }
  | { type: 'tool:result'; toolCallId: string; output: string; isError: boolean; parentToolUseId?: string }
  // `claudeSessionId` is Claude's OWN transcript uuid, and the only id `/chat/sessions/:id` can resolve.
  // `sessionId` is officer's per-connection key (websocket.ts mints it with `msg.sessionId || randomUUID()`),
  // which addresses nothing after the socket closes.
  | { type: 'result'; sessionId: string; cost: MessageCost; claudeSessionId?: string }
  | { type: 'sync:messages'; sessionId: string; messages: Message[]; isGenerating: boolean; streamingText: string }
  /**
   * Claude's transcript uuid, sent at the *start* of a turn. Makes the address bar a permalink before the
   * turn produces anything, which is what makes a mid-turn refresh recoverable at all.
   */
  | { type: 'session:claude'; claudeSessionId: string }
  /**
   * Answer to an `attach`: this socket is bound to a live turn again. Carries no messages by design — the
   * transcript came over HTTP a moment ago and there is no shared id to reconcile the two records by, so
   * this hands over the rest of the turn and the half-written paragraph, and nothing that would double up.
   */
  | {
      type: 'sync:live';
      sessionId: string;
      isGenerating: boolean;
      streamingText: string;
      cursor: number;
      /**
       * Background tasks started and not yet finished. The single exception to "carries no messages": a
       * task row has no counterpart in Claude's transcript — officer synthesises it — so rebuilding from
       * the file cannot bring one back, and the tray came up empty on a refresh while work was still
       * running. Finished tasks are excluded server-side.
       */
      runningTasks: RunningTask[];
    }
  | { type: 'error'; message: string; errorCode?: string }
  | { type: 'stopped' }
  /** The agent process went away mid-turn. Recoverable — see the `cutoff` message role. */
  | { type: 'cut-off' }
  | { type: 'disconnected' }
  | { type: 'task:started'; taskId: string; description: string; taskType?: string }
  | { type: 'task:notification'; taskId: string; status: 'completed' | 'failed' | 'stopped'; summary: string }
  /** Context compaction beginning and ending. See the `compact` message role. */
  | { type: 'compact:start'; trigger: 'manual' | 'auto' }
  | { type: 'compact:done'; trigger: 'manual' | 'auto'; preTokens: number; durationMs?: number };

export type Message = {
  id: string;
  timestamp: number;
  role: 'user' | 'assistant' | 'tool';
  text?: string;
  model?: string;
  cost?: MessageCost;
  toolCallId?: string;
  toolName?: string;
  toolInput?: Record<string, unknown>;
  output?: string;
  isError?: boolean;
  parentToolUseId?: string;
};

export type SlashCommand = {
  command: string;
  description?: string;
  execute: (args: string, sessionId: string) => Promise<{ success: boolean; feedback: string }>;
};
