heyio 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +1 -0
  2. package/dist/api/server.js +11 -4
  3. package/dist/chat/attachments.js +76 -0
  4. package/dist/copilot/agents.js +37 -3
  5. package/dist/copilot/ceremonies.js +13 -6
  6. package/dist/copilot/orchestrator.js +23 -5
  7. package/dist/copilot/squad-tools.js +18 -1
  8. package/dist/copilot/system-message.js +4 -0
  9. package/dist/copilot/tools.js +43 -5
  10. package/dist/store/conversations.js +14 -2
  11. package/dist/store/db.js +7 -0
  12. package/package.json +1 -1
  13. package/web-dist/assets/{AuditLogView-eyz02XQx.js → AuditLogView-Ds7LwE01.js} +1 -1
  14. package/web-dist/assets/ChatView-90GHN5-3.js +11 -0
  15. package/web-dist/assets/{FeedView-Clis71Lg.js → FeedView-RlM4m0Dp.js} +1 -1
  16. package/web-dist/assets/{HistoryView-B1EJ2Q9S.js → HistoryView-Vrb5nk4l.js} +1 -1
  17. package/web-dist/assets/{LoginView-bg8Sl_Gw.js → LoginView-BVt0eVoK.js} +1 -1
  18. package/web-dist/assets/{McpView-CBepc2JJ.js → McpView-DMg1Ieji.js} +1 -1
  19. package/web-dist/assets/{SchedulesView-DDFUUMPG.js → SchedulesView-CvasMUhn.js} +1 -1
  20. package/web-dist/assets/{SettingsView-BFfDRToB.js → SettingsView-DqNi_5IM.js} +1 -1
  21. package/web-dist/assets/{SkillsView-Dv-337Yq.js → SkillsView-CufZGrhf.js} +1 -1
  22. package/web-dist/assets/{SquadDetailView-p4c6XQ5a.js → SquadDetailView-DmseEdEl.js} +1 -1
  23. package/web-dist/assets/{SquadHealthView-IjOdIla7.js → SquadHealthView-D4kmk8OD.js} +1 -1
  24. package/web-dist/assets/{SquadsView-C5wQPkBA.js → SquadsView-CVB2pCwd.js} +1 -1
  25. package/web-dist/assets/{UsageView-BcHfPX-N.js → UsageView-xEe1ZUp4.js} +1 -1
  26. package/web-dist/assets/{WikiView-CKBw5FOf.js → WikiView-ADnv-80y.js} +7 -12
  27. package/web-dist/assets/{api-DPUP4xY9.js → api-2XP2yG1A.js} +1 -1
  28. package/web-dist/assets/{arrow-left-BMJZvMdZ.js → arrow-left-BsbWSKAE.js} +1 -1
  29. package/web-dist/assets/file-text-Dn2yDZCL.js +6 -0
  30. package/web-dist/assets/{git-branch-BF40208L.js → git-branch-wkQUgAGS.js} +1 -1
  31. package/web-dist/assets/{index-CQ_szaoT.css → index-Cd9kiyp4.css} +1 -1
  32. package/web-dist/assets/{index-DSAUDIwK.js → index-DI8QgDXv.js} +35 -35
  33. package/web-dist/assets/{plus-aHVgAXQB.js → plus-BOXwfDpA.js} +1 -1
  34. package/web-dist/assets/{save-jn5LGsCv.js → save-CTfFmP-U.js} +1 -1
  35. package/web-dist/assets/{search-DUqJxRNn.js → search-BnsxENnH.js} +1 -1
  36. package/web-dist/assets/{trash-2-B9ibnt3n.js → trash-2-C86P23n5.js} +1 -1
  37. package/web-dist/assets/{triangle-alert-Cindqw1C.js → triangle-alert-MLnkHsRL.js} +1 -1
  38. package/web-dist/assets/{x-BDIsved4.js → x-Cw7ZjfeL.js} +1 -1
  39. package/web-dist/index.html +2 -2
  40. package/web-dist/assets/ChatView-DXpE1NMj.js +0 -1
package/README.md CHANGED
@@ -11,6 +11,7 @@ IO is a personal AI assistant daemon built on the GitHub Copilot SDK. It runs 24
11
11
  - **Copilot SDK Integration** — powered by GitHub's Copilot SDK for LLM conversations with tool calling
12
12
  - **Multi-Interface** — Web dashboard + Telegram bot + HTTP API
13
13
  - **Web Frontend** — Vue 3 dashboard with chat, squad management, skills, and agent activity views
14
+ - **Web Chat Attachments** — Attach images/files in web chat (10MB per file, 25MB per message) with multimodal handoff
14
15
  - **Persistent Memory** — wiki-based knowledge base stored at `~/.io/wiki/`
15
16
  - **Squad System** — persistent project teams with **named specialist agents** themed from pop culture universes (dynamically selected at squad creation)
16
17
  - **Skills** — modular skill system; install from git repos or the [skills.sh](https://skills.sh) registry
@@ -22,6 +22,7 @@ import { saveMessage, getConversation, listConversations, searchConversations, d
22
22
  import { getTokenUsageSummary, getTokenUsageBySquad, getTokenUsageByAgent, getDailyTokenUsage, } from "../store/token-usage.js";
23
23
  import { DEFAULT_MODEL_PRICING } from "../copilot/token-tracker.js";
24
24
  import { randomUUID } from "node:crypto";
25
+ import { validateMessageAttachments } from "../chat/attachments.js";
25
26
  const __filename = fileURLToPath(import.meta.url);
26
27
  const __dirname = dirname(__filename);
27
28
  const sseClients = [];
@@ -33,7 +34,7 @@ function broadcast(event, data) {
33
34
  }
34
35
  export async function startApiServer(config) {
35
36
  const app = express();
36
- app.use(express.json());
37
+ app.use(express.json({ limit: "30mb" }));
37
38
  // Serve static web frontend
38
39
  const webDistPath = resolve(__dirname, "..", "..", "web-dist");
39
40
  app.use(express.static(webDistPath));
@@ -64,14 +65,20 @@ export async function startApiServer(config) {
64
65
  });
65
66
  // --- Chat (SSE-streamed response) ---
66
67
  app.post("/api/message", async (req, res) => {
67
- const { prompt, conversationId: clientConvId } = req.body;
68
+ const { prompt, conversationId: clientConvId, attachments: rawAttachments } = req.body;
68
69
  if (!prompt || typeof prompt !== "string") {
69
70
  res.status(400).json({ error: "prompt is required" });
70
71
  return;
71
72
  }
73
+ const validation = validateMessageAttachments(rawAttachments);
74
+ if (!validation.ok) {
75
+ res.status(400).json({ error: validation.error });
76
+ return;
77
+ }
78
+ const attachments = validation.attachments;
72
79
  const conversationId = (typeof clientConvId === "string" && clientConvId) ? clientConvId : randomUUID();
73
80
  // Persist the user message
74
- saveMessage(conversationId, "user", prompt, "web");
81
+ saveMessage(conversationId, "user", prompt, "web", attachments);
75
82
  // Switch to SSE streaming to avoid Cloudflare 524 timeouts
76
83
  res.setHeader("Content-Type", "text/event-stream");
77
84
  res.setHeader("Cache-Control", "no-cache");
@@ -99,7 +106,7 @@ export async function startApiServer(config) {
99
106
  // Also broadcast to other SSE listeners (e.g. ChatOverlay)
100
107
  broadcast("message_delta", { content, done: false });
101
108
  }
102
- });
109
+ }, attachments);
103
110
  }
104
111
  catch (err) {
105
112
  if (!closed) {
@@ -0,0 +1,76 @@
1
+ export const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
2
+ export const MAX_TOTAL_ATTACHMENT_BYTES = 25 * 1024 * 1024;
3
+ const BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
4
+ function isValidBase64(value) {
5
+ if (!value || value.length % 4 !== 0)
6
+ return false;
7
+ return BASE64_RE.test(value);
8
+ }
9
+ function isMessageAttachment(value) {
10
+ if (!value || typeof value !== "object")
11
+ return false;
12
+ const candidate = value;
13
+ return (typeof candidate.name === "string" &&
14
+ candidate.name.trim().length > 0 &&
15
+ typeof candidate.mimeType === "string" &&
16
+ candidate.mimeType.trim().length > 0 &&
17
+ typeof candidate.size === "number" &&
18
+ Number.isFinite(candidate.size) &&
19
+ candidate.size >= 0 &&
20
+ typeof candidate.content === "string");
21
+ }
22
+ export function validateMessageAttachments(input) {
23
+ if (input === undefined || input === null) {
24
+ return { ok: true, attachments: [] };
25
+ }
26
+ if (!Array.isArray(input)) {
27
+ return { ok: false, error: "attachments must be an array" };
28
+ }
29
+ let totalSize = 0;
30
+ const attachments = [];
31
+ for (const raw of input) {
32
+ if (!isMessageAttachment(raw)) {
33
+ return {
34
+ ok: false,
35
+ error: "each attachment must include name, mimeType, size, and content",
36
+ };
37
+ }
38
+ if (raw.size > MAX_ATTACHMENT_BYTES) {
39
+ return { ok: false, error: "each attachment must be 10MB or smaller" };
40
+ }
41
+ if (!isValidBase64(raw.content)) {
42
+ return { ok: false, error: `attachment "${raw.name}" has invalid base64 content` };
43
+ }
44
+ const trimmedName = raw.name.trim();
45
+ const trimmedMimeType = raw.mimeType.trim();
46
+ if (!trimmedName || !trimmedMimeType) {
47
+ return { ok: false, error: "attachment name and mimeType are required" };
48
+ }
49
+ totalSize += raw.size;
50
+ attachments.push({
51
+ name: trimmedName,
52
+ mimeType: trimmedMimeType,
53
+ size: raw.size,
54
+ content: raw.content,
55
+ });
56
+ }
57
+ if (totalSize > MAX_TOTAL_ATTACHMENT_BYTES) {
58
+ return { ok: false, error: "total attachment size must be 25MB or smaller" };
59
+ }
60
+ return { ok: true, attachments };
61
+ }
62
+ export function toCopilotBlobAttachments(attachments) {
63
+ return attachments.map((attachment) => ({
64
+ type: "blob",
65
+ data: attachment.content,
66
+ mimeType: attachment.mimeType,
67
+ displayName: attachment.name,
68
+ }));
69
+ }
70
+ export function buildAttachmentSummary(attachments) {
71
+ if (attachments.length === 0)
72
+ return "";
73
+ const lines = attachments.map((attachment) => `- ${attachment.name} (${attachment.mimeType}, ${Math.round(attachment.size / 1024)}KB)`);
74
+ return `\n\n[Attachments]\n${lines.join("\n")}`;
75
+ }
76
+ //# sourceMappingURL=attachments.js.map
@@ -12,6 +12,7 @@ import { PATHS } from "../paths.js";
12
12
  import { createSquadTools } from "./squad-tools.js";
13
13
  import { loadSkillDirectories } from "./skills.js";
14
14
  import { getMcpServersForSession } from "../mcp/registry.js";
15
+ import { buildAttachmentSummary, toCopilotBlobAttachments } from "../chat/attachments.js";
15
16
  import { existsSync, mkdirSync } from "node:fs";
16
17
  import { join } from "node:path";
17
18
  import { exec } from "node:child_process";
@@ -77,7 +78,7 @@ export async function stopTask(taskId) {
77
78
  updateAgentStatus(task.agent_id, "idle");
78
79
  }
79
80
  }
80
- export async function delegateTask(squadId, task, instanceId) {
81
+ export async function delegateTask(squadId, task, instanceId, attachments = []) {
81
82
  const lead = getLeadForSquad(squadId);
82
83
  if (!lead) {
83
84
  throw new Error("Squad has no team lead. Add a lead agent first.");
@@ -102,6 +103,29 @@ export async function delegateTask(squadId, task, instanceId) {
102
103
  const agentRoster = agents
103
104
  .map((a) => `- ${a.character_name} (${a.role_title})${a.is_lead ? " [LEAD]" : ""}${a.is_qa ? " [QA]" : ""}${a.is_test ? " [TEST]" : ""}`)
104
105
  .join("\n");
106
+ // Load squad wiki pages as immutable knowledge context
107
+ const { listPages, readPage } = await import("../wiki/fs.js");
108
+ const wikiPrefix = `squads/${squadSlug}`;
109
+ let wikiKnowledge = "";
110
+ try {
111
+ const pages = await listPages(wikiPrefix);
112
+ const pageContents = [];
113
+ for (const page of pages.slice(0, 20)) { // Cap at 20 pages to avoid token overload
114
+ try {
115
+ const content = await readPage(`${wikiPrefix}/${page}`);
116
+ pageContents.push(`### ${page}\n${content}`);
117
+ }
118
+ catch {
119
+ // Skip unreadable pages
120
+ }
121
+ }
122
+ if (pageContents.length > 0) {
123
+ wikiKnowledge = `\n## Squad Knowledge Base\nThe following wiki pages contain important decisions, conventions, and context for this squad. Follow these as authoritative guidance:\n\n${pageContents.join("\n\n---\n\n")}\n`;
124
+ }
125
+ }
126
+ catch {
127
+ // Wiki not available — proceed without
128
+ }
105
129
  const systemMessage = `# Squad Team Lead: ${lead.character_name}
106
130
 
107
131
  You are ${lead.character_name}, the team lead for this squad. Your role is STRICTLY coordination — you do NOT write code, tests, or implementation of any kind.
@@ -129,7 +153,9 @@ ${agentRoster}
129
153
  - Use \`--comment\` with "LGTM" for approvals (not \`--approve\`)
130
154
  - Always use the gh CLI for GitHub interactions
131
155
  - Merge criteria: all veto-capable members have posted approving comments + CI passes + no conflicts
132
-
156
+ - When work is complete, ALWAYS notify the user via feed_post with a summary of what was done
157
+ - Consult the squad wiki (wiki_read, wiki_search) for additional context beyond what's provided below
158
+ ${wikiKnowledge}
133
159
  ${lead.persona ? `## Personality:\n${lead.persona}` : ""}
134
160
  `;
135
161
  let result;
@@ -169,6 +195,11 @@ ${lead.persona ? `## Personality:\n${lead.persona}` : ""}
169
195
  agent: lead.character_name,
170
196
  role: lead.role_title,
171
197
  task,
198
+ attachments: attachments.map((attachment) => ({
199
+ name: attachment.name,
200
+ mimeType: attachment.mimeType,
201
+ size: attachment.size,
202
+ })),
172
203
  });
173
204
  // Capture streaming message deltas and broadcast via SSE
174
205
  let accumulatedMessage = "";
@@ -186,7 +217,10 @@ ${lead.persona ? `## Personality:\n${lead.persona}` : ""}
186
217
  }
187
218
  });
188
219
  try {
189
- const response = await session.sendAndWait({ prompt: `Task delegated to you:\n\n${task}` }, 600_000);
220
+ const response = await session.sendAndWait({
221
+ prompt: `Task delegated to you:\n\n${task}${buildAttachmentSummary(attachments)}`,
222
+ attachments: toCopilotBlobAttachments(attachments),
223
+ }, 600_000);
190
224
  result = response?.data?.content ?? "Task completed (no response content).";
191
225
  // Record the final message event if we have meaningful content
192
226
  if (accumulatedMessage.trim()) {
@@ -4,6 +4,7 @@ import { getLeadForSquad, getAgentsForSquad, getSquad } from "../store/squads.js
4
4
  import { selectModel } from "./model-router.js";
5
5
  import { postFeedItem } from "../store/feed.js";
6
6
  import { attachTokenTracker } from "./token-tracker.js";
7
+ import { buildAttachmentSummary, toCopilotBlobAttachments } from "../chat/attachments.js";
7
8
  function buildFacilitatorPrompt(lead, agents, task) {
8
9
  const roster = agents
9
10
  .filter((a) => !a.is_lead)
@@ -76,7 +77,7 @@ Keep your response focused and concise — this is a planning meeting, not imple
76
77
  ${agent.persona ? `\n## Your Style:\n${agent.persona}` : ""}
77
78
  `;
78
79
  }
79
- export async function planningMeeting(squadId, task) {
80
+ export async function planningMeeting(squadId, task, attachments = []) {
80
81
  const lead = getLeadForSquad(squadId);
81
82
  if (!lead) {
82
83
  throw new Error("Squad has no team lead. Add a lead agent first.");
@@ -99,7 +100,10 @@ export async function planningMeeting(squadId, task) {
99
100
  });
100
101
  const flushTokens = attachTokenTracker(session, { squadId, agentId: agent.id });
101
102
  try {
102
- const response = await session.sendAndWait({ prompt: "Please provide your planning input for this task." }, 60_000);
103
+ const response = await session.sendAndWait({
104
+ prompt: `Please provide your planning input for this task.${buildAttachmentSummary(attachments)}`,
105
+ attachments: toCopilotBlobAttachments(attachments),
106
+ }, 60_000);
103
107
  return {
104
108
  agent: agent.character_name,
105
109
  role: agent.role_title,
@@ -134,7 +138,10 @@ export async function planningMeeting(squadId, task) {
134
138
  let plan;
135
139
  try {
136
140
  const prompt = `Here is the input gathered from your team:\n\n${inputsSummary}\n\nNow synthesize this into a clear, structured action plan.`;
137
- const response = await facilitatorSession.sendAndWait({ prompt }, 120_000);
141
+ const response = await facilitatorSession.sendAndWait({
142
+ prompt,
143
+ attachments: toCopilotBlobAttachments(attachments),
144
+ }, 120_000);
138
145
  plan = response?.data?.content ?? "Planning meeting completed but no plan was produced.";
139
146
  }
140
147
  finally {
@@ -146,8 +153,8 @@ export async function planningMeeting(squadId, task) {
146
153
  participants: [lead.character_name, ...inputs.map((i) => i.agent)],
147
154
  };
148
155
  }
149
- export async function squadMeeting(squadId, task, executeAfter) {
150
- const result = await planningMeeting(squadId, task);
156
+ export async function squadMeeting(squadId, task, executeAfter, attachments = []) {
157
+ const result = await planningMeeting(squadId, task, attachments);
151
158
  const summary = `## Planning Meeting Complete\n\n**Participants:** ${result.participants.join(", ")}\n\n${result.plan}`;
152
159
  if (!executeAfter) {
153
160
  // Post to feed and wait for user to trigger execution
@@ -159,7 +166,7 @@ export async function squadMeeting(squadId, task, executeAfter) {
159
166
  // Execute: delegate with the plan as additional context
160
167
  const { delegateTask } = await import("./agents.js");
161
168
  const enrichedTask = `${task}\n\n---\n## Approved Plan (from team meeting)\n${result.plan}`;
162
- const execResult = await delegateTask(squadId, enrichedTask);
169
+ const execResult = await delegateTask(squadId, enrichedTask, undefined, attachments);
163
170
  return `Meeting held, then executed.\n\n${summary}\n\n---\n## Execution Result\n${execResult}`;
164
171
  }
165
172
  //# sourceMappingURL=ceremonies.js.map
@@ -7,9 +7,11 @@ import { loadSkillDirectories } from "./skills.js";
7
7
  import { getMcpServersForSession } from "../mcp/registry.js";
8
8
  import { resetClient } from "./client.js";
9
9
  import { addAuditEntry } from "../store/audit-log.js";
10
+ import { buildAttachmentSummary, toCopilotBlobAttachments, } from "../chat/attachments.js";
10
11
  let orchestratorSession;
11
12
  let sessionCreatePromise;
12
13
  let healthCheckInterval;
14
+ let activeMessageAttachments = [];
13
15
  const messageQueue = [];
14
16
  let processing = false;
15
17
  export async function initOrchestrator(client, opts) {
@@ -82,9 +84,17 @@ function startHealthCheck(client, opts) {
82
84
  }, 30_000);
83
85
  healthCheckInterval.unref();
84
86
  }
85
- export async function sendToOrchestrator(prompt, source, callback) {
86
- addAuditEntry("message_received", `Message from ${source}: ${prompt.slice(0, 200)}`, { source, prompt: prompt.slice(0, 1000) });
87
- messageQueue.push({ prompt, source, callback });
87
+ export async function sendToOrchestrator(prompt, source, callback, attachments = []) {
88
+ addAuditEntry("message_received", `Message from ${source}: ${prompt.slice(0, 200)}`, {
89
+ source,
90
+ prompt: prompt.slice(0, 1000),
91
+ attachments: attachments.map((attachment) => ({
92
+ name: attachment.name,
93
+ mimeType: attachment.mimeType,
94
+ size: attachment.size,
95
+ })),
96
+ });
97
+ messageQueue.push({ prompt, source, callback, attachments });
88
98
  if (!processing)
89
99
  processQueue();
90
100
  }
@@ -109,7 +119,8 @@ async function executeOnSession(msg) {
109
119
  msg.callback("Error: Orchestrator session not initialized.", true);
110
120
  return;
111
121
  }
112
- const taggedPrompt = `[via ${msg.source}] ${msg.prompt}`;
122
+ activeMessageAttachments = msg.attachments;
123
+ const taggedPrompt = `[via ${msg.source}] ${msg.prompt}${buildAttachmentSummary(msg.attachments)}`;
113
124
  let accumulated = "";
114
125
  const unsubscribe = orchestratorSession.on("assistant.message_delta", (event) => {
115
126
  const delta = event.data?.deltaContent ?? "";
@@ -117,11 +128,15 @@ async function executeOnSession(msg) {
117
128
  msg.callback(accumulated, false);
118
129
  });
119
130
  try {
120
- const response = await orchestratorSession.sendAndWait({ prompt: taggedPrompt }, 600_000);
131
+ const response = await orchestratorSession.sendAndWait({
132
+ prompt: taggedPrompt,
133
+ attachments: toCopilotBlobAttachments(msg.attachments),
134
+ }, 600_000);
121
135
  const finalContent = response?.data?.content ?? accumulated;
122
136
  msg.callback(finalContent, true);
123
137
  }
124
138
  finally {
139
+ activeMessageAttachments = [];
125
140
  unsubscribe();
126
141
  }
127
142
  }
@@ -132,4 +147,7 @@ export function feedAgentResult(taskId, agentName, result, callback) {
132
147
  export function getOrchestratorSession() {
133
148
  return orchestratorSession;
134
149
  }
150
+ export function getActiveMessageAttachments() {
151
+ return activeMessageAttachments;
152
+ }
135
153
  //# sourceMappingURL=orchestrator.js.map
@@ -136,11 +136,28 @@ export function createSquadTools(squadSlug, squadId, repoUrl) {
136
136
  return `Error: cannot execute commands outside the project directory`;
137
137
  }
138
138
  try {
139
+ // Ensure GitHub CLI auth is available — prefer GH_TOKEN from env,
140
+ // fall back to extracting from gh auth if available
141
+ const shellEnv = { ...process.env, GH_PROMPT_DISABLED: "1" };
142
+ if (!shellEnv.GH_TOKEN && !shellEnv.GITHUB_TOKEN) {
143
+ try {
144
+ const { stdout: token } = await execAsync("gh auth token", {
145
+ timeout: 5_000,
146
+ env: process.env,
147
+ });
148
+ if (token.trim()) {
149
+ shellEnv.GH_TOKEN = token.trim();
150
+ }
151
+ }
152
+ catch {
153
+ // gh auth not available — agents won't be able to push
154
+ }
155
+ }
139
156
  const { stdout } = await execAsync(command, {
140
157
  cwd: workDir,
141
158
  timeout: 120_000,
142
159
  maxBuffer: 2 * 1024 * 1024,
143
- env: { ...process.env, GH_PROMPT_DISABLED: "1" },
160
+ env: shellEnv,
144
161
  });
145
162
  return stdout.trim() || "(no output)";
146
163
  }
@@ -25,6 +25,7 @@ You are IO, a personal AI assistant daemon. You run 24/7 on the user's machine,
25
25
  - Always delegate code work to the relevant squad — never implement directly
26
26
  - Always delegate to the team lead, never to a specific agent (unless explicitly named by the user)
27
27
  - Never delegate unless explicitly asked — creating an issue ≠ request to start work
28
+ - When delegating, ALWAYS instruct the squad to notify the user via the inbox (feed_post) when work is complete
28
29
  - When creating squads, research the universe dynamically — never use hardcoded character lists
29
30
  - **Always use the gh CLI** for all GitHub interactions (repos, issues, PRs, releases, actions, etc.). Only fall back to the GitHub API or other methods if gh is unavailable or cannot accomplish the task.
30
31
  - For complex tasks involving multiple specialists, use squad_meeting to have the team plan together before executing. Use squad_delegate for straightforward single-domain tasks.
@@ -37,9 +38,12 @@ If a project has a squad assigned to it, you (the orchestrator) must NEVER:
37
38
  - Research, analyze, or investigate the project's code, issues, or state yourself
38
39
  - Attempt any work — even preliminary analysis — before delegating
39
40
  - "Look into" or "check on" something before passing it to the squad
41
+ - Wait for the squad to finish before responding to the user
40
42
 
41
43
  When a request comes in about a squad-owned project, you IMMEDIATELY delegate to that squad's team lead with no pre-processing. The squad handles ALL work including research, analysis, planning, and execution.
42
44
 
45
+ After delegating, IMMEDIATELY confirm to the user that the task is in the squad's hands (e.g., "I've handed this off to [squad name]. They'll work on it and post updates to your inbox."). Do NOT block or wait for the squad to complete — they work asynchronously in the background.
46
+
43
47
  The ONLY thing you are allowed to do regarding a squad-owned project (without delegating) is:
44
48
  - Answer questions about what the squad has already done (using feed/task history)
45
49
  - Report squad status, task progress, or past deliverables
@@ -159,8 +159,19 @@ export function createTools() {
159
159
  }),
160
160
  handler: async ({ squad_id, task, instance_id }) => {
161
161
  const { delegateTask } = await import("./agents.js");
162
- addAuditEntry("task_delegated", `Task delegated to squad ${squad_id}: ${task.slice(0, 200)}`, { squad_id, task: task.slice(0, 1000), instance_id }, { squad_id });
163
- const result = await delegateTask(squad_id, task, instance_id);
162
+ const { getActiveMessageAttachments } = await import("./orchestrator.js");
163
+ const attachments = getActiveMessageAttachments();
164
+ addAuditEntry("task_delegated", `Task delegated to squad ${squad_id}: ${task.slice(0, 200)}`, {
165
+ squad_id,
166
+ task: task.slice(0, 1000),
167
+ instance_id,
168
+ attachments: attachments.map((attachment) => ({
169
+ name: attachment.name,
170
+ mimeType: attachment.mimeType,
171
+ size: attachment.size,
172
+ })),
173
+ }, { squad_id });
174
+ const result = await delegateTask(squad_id, task, instance_id, attachments);
164
175
  return result;
165
176
  },
166
177
  }),
@@ -175,8 +186,19 @@ export function createTools() {
175
186
  }),
176
187
  handler: async ({ squad_id, task, execute_after }) => {
177
188
  const { squadMeeting } = await import("./ceremonies.js");
178
- addAuditEntry("squad_meeting", `Planning meeting started for squad ${squad_id}: ${task.slice(0, 200)}`, { squad_id, task: task.slice(0, 1000), execute_after }, { squad_id });
179
- return await squadMeeting(squad_id, task, execute_after);
189
+ const { getActiveMessageAttachments } = await import("./orchestrator.js");
190
+ const attachments = getActiveMessageAttachments();
191
+ addAuditEntry("squad_meeting", `Planning meeting started for squad ${squad_id}: ${task.slice(0, 200)}`, {
192
+ squad_id,
193
+ task: task.slice(0, 1000),
194
+ execute_after,
195
+ attachments: attachments.map((attachment) => ({
196
+ name: attachment.name,
197
+ mimeType: attachment.mimeType,
198
+ size: attachment.size,
199
+ })),
200
+ }, { squad_id });
201
+ return await squadMeeting(squad_id, task, execute_after, attachments);
180
202
  },
181
203
  }),
182
204
  defineTool("squad_task_status", {
@@ -336,12 +358,28 @@ export function createTools() {
336
358
  const { promisify } = await import("node:util");
337
359
  const { homedir } = await import("node:os");
338
360
  const execAsync = promisify(exec);
361
+ // Ensure GitHub CLI auth is available in the shell environment
362
+ const shellEnv = { ...process.env, GH_PROMPT_DISABLED: "1" };
363
+ if (!shellEnv.GH_TOKEN && !shellEnv.GITHUB_TOKEN) {
364
+ try {
365
+ const { stdout: token } = await execAsync("gh auth token", {
366
+ timeout: 5_000,
367
+ env: process.env,
368
+ });
369
+ if (token.trim()) {
370
+ shellEnv.GH_TOKEN = token.trim();
371
+ }
372
+ }
373
+ catch {
374
+ // gh auth not available
375
+ }
376
+ }
339
377
  try {
340
378
  const { stdout } = await execAsync(command, {
341
379
  cwd: cwd ?? homedir(),
342
380
  timeout: 60_000,
343
381
  maxBuffer: 1024 * 1024,
344
- env: { ...process.env, GH_PROMPT_DISABLED: "1" },
382
+ env: shellEnv,
345
383
  });
346
384
  const output = stdout.trim() || "(no output)";
347
385
  addAuditEntry("shell_command", `Command: ${command.slice(0, 200)}`, { command, cwd, output: output.slice(0, 500), exit_code: 0 });
@@ -1,5 +1,16 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { getDb } from "./db.js";
3
+ function parseAttachments(value) {
4
+ if (!value)
5
+ return [];
6
+ try {
7
+ const parsed = JSON.parse(value);
8
+ return Array.isArray(parsed) ? parsed : [];
9
+ }
10
+ catch {
11
+ return [];
12
+ }
13
+ }
3
14
  function toMessage(row) {
4
15
  return {
5
16
  id: row.id,
@@ -7,6 +18,7 @@ function toMessage(row) {
7
18
  role: row.role,
8
19
  content: row.content,
9
20
  source: row.source,
21
+ attachments: parseAttachments(row.attachments),
10
22
  createdAt: row.created_at,
11
23
  };
12
24
  }
@@ -19,10 +31,10 @@ function toSummary(row) {
19
31
  updatedAt: row.updated_at,
20
32
  };
21
33
  }
22
- export function saveMessage(conversationId, role, content, source) {
34
+ export function saveMessage(conversationId, role, content, source, attachments = []) {
23
35
  const db = getDb();
24
36
  const id = randomUUID();
25
- db.prepare("INSERT INTO conversation_messages (id, conversation_id, role, content, source) VALUES (?, ?, ?, ?, ?)").run(id, conversationId, role, content, source);
37
+ db.prepare("INSERT INTO conversation_messages (id, conversation_id, role, content, source, attachments) VALUES (?, ?, ?, ?, ?, ?)").run(id, conversationId, role, content, source, JSON.stringify(attachments));
26
38
  const row = db
27
39
  .prepare("SELECT * FROM conversation_messages WHERE id = ?")
28
40
  .get(id);
package/dist/store/db.js CHANGED
@@ -239,6 +239,13 @@ function runMigrations(db) {
239
239
  `);
240
240
  setSchemaVersion(db, 7);
241
241
  }
242
+ if (version < 8) {
243
+ db.exec(`
244
+ ALTER TABLE conversation_messages
245
+ ADD COLUMN attachments TEXT NOT NULL DEFAULT '[]';
246
+ `);
247
+ setSchemaVersion(db, 8);
248
+ }
242
249
  }
243
250
  function getSchemaVersion(db) {
244
251
  const row = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "heyio",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "description": "IO — a personal AI assistant daemon built on the GitHub Copilot SDK",
5
5
  "bin": {
6
6
  "io": "dist/index.js"
@@ -1,4 +1,4 @@
1
- import{l as W,o as X,s as Z,u as a,k as d,h as o,n as P,D as A,a as V,m as S,z as l,T as f,N as T,F as h,w,O as F,j as b,v as n,g as _,q as ee}from"./index-DSAUDIwK.js";import{b as L}from"./api-DPUP4xY9.js";/**
1
+ import{l as W,o as X,s as Z,u as a,k as d,h as o,n as P,D as A,a as V,m as S,z as l,T as f,N as T,F as h,w,O as F,j as b,v as n,g as _,q as ee}from"./index-DI8QgDXv.js";import{b as L}from"./api-2XP2yG1A.js";/**
2
2
  * @license lucide-vue-next v0.474.0 - ISC
3
3
  *
4
4
  * This source code is licensed under the ISC license.
@@ -0,0 +1,11 @@
1
+ import{l as U,o as R,G as W,Q as B,s as Y,u as a,k as l,h as o,D as t,j as y,F as T,w as A,z as v,n as $,T as G,O as Q,i as h,S as J,d as Z,q as k,v as x,g as I,f as ee,p as E}from"./index-DI8QgDXv.js";import{F as z}from"./file-text-Dn2yDZCL.js";import{X as te}from"./x-Cw7ZjfeL.js";/**
2
+ * @license lucide-vue-next v0.474.0 - ISC
3
+ *
4
+ * This source code is licensed under the ISC license.
5
+ * See the LICENSE file in the root directory of this source tree.
6
+ */const F=U("ImageIcon",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2",key:"1m3agn"}],["circle",{cx:"9",cy:"9",r:"2",key:"af1f0g"}],["path",{d:"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21",key:"1xmnt7"}]]);/**
7
+ * @license lucide-vue-next v0.474.0 - ISC
8
+ *
9
+ * This source code is licensed under the ISC license.
10
+ * See the LICENSE file in the root directory of this source tree.
11
+ */const se=U("PaperclipIcon",[["path",{d:"M13.234 20.252 21 12.3",key:"1cbrk9"}],["path",{d:"m16 6-8.414 8.586a2 2 0 0 0 0 2.828 2 2 0 0 0 2.828 0l8.414-8.586a4 4 0 0 0 0-5.656 4 4 0 0 0-5.656 0l-8.415 8.585a6 6 0 1 0 8.486 8.486",key:"1pkts6"}]]),L=10*1024*1024,P=25*1024*1024;function _(n){return n<1024?`${n} B`:n<1024*1024?`${(n/1024).toFixed(1)} KB`:`${(n/(1024*1024)).toFixed(1)} MB`}function S(n){return n.mimeType.startsWith("image/")}function ae(n){return`data:${n.mimeType};base64,${n.content}`}async function ne(n){const i=await new Promise((d,g)=>{const m=new FileReader;m.onload=()=>d(String(m.result??"")),m.onerror=()=>g(new Error("Unable to read file")),m.readAsDataURL(n)}),c=i.indexOf(",");if(c===-1)throw new Error(`Unable to parse file content for ${n.name}`);return{name:n.name,mimeType:n.type||"application/octet-stream",size:n.size,content:i.slice(c+1)}}function oe(n){for(const c of n)if(c.size>L)return{ok:!1,error:`"${c.name}" exceeds the 10MB per-file limit.`};return n.reduce((c,d)=>c+d.size,0)>P?{ok:!1,error:"Attachments exceed the 25MB per-message limit."}:{ok:!0}}const re={class:"flex flex-col h-full"},le={key:0,class:"flex items-center justify-center h-full"},ce={key:0,class:"mb-2 space-y-2"},ie=["src","alt"],ue={class:"flex items-center gap-2 text-xs"},de={class:"truncate"},me={class:"opacity-70"},pe={key:2,class:"text-muted-foreground"},fe={key:3,class:"inline-block w-2 h-4 bg-current animate-pulse ml-1"},ve={key:0,class:"mb-2 space-y-2"},he={class:"flex flex-wrap gap-2"},ge={class:"max-w-[170px] truncate"},ye={class:"opacity-70"},xe=["onClick"],_e={class:"text-xs text-muted-foreground"},be={key:1,class:"text-xs text-destructive mb-2"},ke={class:"flex gap-2 items-end"},we=["disabled"],Te=["disabled"],Ce=R({__name:"ChatView",setup(n){const i=W(),c=x(""),d=x(""),g=x(),m=x(),p=x([]),b=x(!1),j=I(()=>p.value.reduce((e,r)=>e+r.size,0)),D=I(()=>!i.isStreaming&&(c.value.trim().length>0||p.value.length>0));async function C(e){if(!e||e.length===0)return;d.value="";const r=[];try{for(const f of Array.from(e))r.push(await ne(f))}catch(f){d.value=(f==null?void 0:f.message)??"Unable to read one or more files.";return}const s=[...p.value,...r],u=oe(s);if(!u.ok){d.value=u.error;return}p.value=s,m.value&&(m.value.value="")}function N(e){p.value.splice(e,1),d.value=""}function O(){var e;(e=m.value)==null||e.click()}function V(e){const r=e.target;C((r==null?void 0:r.files)??null)}async function M(){if(!D.value)return;const e=c.value.trim(),r=[...p.value],s=e||"Please review the attached file(s).";c.value="",p.value=[],d.value="",await i.sendMessage(s,r)}function w(){g.value&&(g.value.scrollTop=g.value.scrollHeight)}function K(e){e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),M())}function q(e){e.preventDefault(),b.value=!0}function H(e){e.preventDefault(),b.value=!1}async function X(e){var r;e.preventDefault(),b.value=!1,await C(((r=e.dataTransfer)==null?void 0:r.files)??null)}return B(()=>i.messages.map(e=>e.content),async()=>{await E(),w()},{deep:!0}),B(()=>i.messages.length,async()=>{await E(),w()}),Y(()=>w()),(e,r)=>(a(),l("div",re,[o("div",{ref_key:"messagesContainer",ref:g,class:"flex-1 overflow-y-auto p-4 space-y-4"},[t(i).messages.length===0?(a(),l("div",le,[...r[1]||(r[1]=[o("div",{class:"text-center text-muted-foreground"},[o("div",{class:"text-4xl mb-3"},"🤖"),o("p",{class:"text-lg font-medium"},"Welcome to IO"),o("p",{class:"text-sm mt-1"},"Send a message to get started.")],-1)])])):y("",!0),(a(!0),l(T,null,A(t(i).messages,s=>(a(),l("div",{key:s.id,class:k(["flex",s.role==="user"?"justify-end":"justify-start"])},[o("div",{class:k(["max-w-[75%] rounded-lg px-4 py-2 text-sm",s.role==="user"?"bg-primary text-primary-foreground":"bg-muted text-foreground"])},[s.attachments.length>0?(a(),l("div",ce,[(a(!0),l(T,null,A(s.attachments,(u,f)=>(a(),l("div",{key:`${s.id}-${f}`,class:"rounded border border-border/50 p-2 bg-background/70 text-foreground"},[t(S)(u)?(a(),l("img",{key:0,src:t(ae)(u),alt:u.name,class:"max-h-44 rounded mb-1 object-contain"},null,8,ie)):y("",!0),o("div",ue,[t(S)(u)?(a(),h(t(F),{key:0,class:"w-3.5 h-3.5"})):(a(),h(t(z),{key:1,class:"w-3.5 h-3.5"})),o("span",de,v(u.name),1),o("span",me,v(t(_)(u.size)),1)])]))),128))])):y("",!0),s.content?(a(),h(ee,{key:1,content:s.content,class:k(s.role==="user"?"prose-invert":"")},null,8,["content","class"])):(a(),l("span",pe,"...")),s.streaming?(a(),l("div",fe)):y("",!0)],2)],2))),128))],512),o("div",{class:k(["border-t border-border p-4",b.value?"bg-accent/40":""]),onDragover:q,onDragleave:H,onDrop:X},[o("input",{ref_key:"fileInput",ref:m,type:"file",multiple:"",class:"hidden",onChange:V},null,544),p.value.length>0?(a(),l("div",ve,[o("div",he,[(a(!0),l(T,null,A(p.value,(s,u)=>(a(),l("div",{key:`${s.name}-${u}`,class:"flex items-center gap-2 rounded border border-border px-2 py-1 text-xs bg-muted"},[t(S)(s)?(a(),h(t(F),{key:0,class:"w-3.5 h-3.5"})):(a(),h(t(z),{key:1,class:"w-3.5 h-3.5"})),o("span",ge,v(s.name),1),o("span",ye,v(t(_)(s.size)),1),o("button",{class:"hover:text-destructive",onClick:f=>N(u)},[$(t(te),{class:"w-3.5 h-3.5"})],8,xe)]))),128))]),o("p",_e,v(t(_)(j.value))+" attached · Max per file "+v(t(_)(t(L)))+" · Max total "+v(t(_)(t(P))),1)])):y("",!0),d.value?(a(),l("p",be,v(d.value),1)):y("",!0),o("div",ke,[o("button",{class:"rounded-md border border-input p-2 hover:bg-accent disabled:opacity-50",disabled:t(i).isStreaming,onClick:O,title:"Attach files"},[$(t(se),{class:"w-4 h-4"})],8,we),G(o("textarea",{"onUpdate:modelValue":r[0]||(r[0]=s=>c.value=s),onKeydown:K,placeholder:"Send a message...",rows:"1",class:"flex-1 resize-none rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring min-h-[40px] max-h-[120px]"},null,544),[[Q,c.value]]),o("button",{onClick:M,disabled:!D.value,class:"rounded-md bg-primary text-primary-foreground p-2 hover:bg-primary/90 disabled:opacity-50 transition-colors"},[t(i).isStreaming?(a(),h(t(Z),{key:1,class:"w-4 h-4"})):(a(),h(t(J),{key:0,class:"w-4 h-4"}))],8,Te)])],34)]))}});export{Ce as default};
@@ -1,4 +1,4 @@
1
- import{l as M,o as D,s as N,u as o,k as n,h as a,F as y,w as b,n as c,D as i,I as V,v as d,g as z,q as f,z as v,r as B,W as k,j as q,f as j}from"./index-DSAUDIwK.js";import{b as C,c as P,a as T}from"./api-DPUP4xY9.js";import{g as W}from"./squad-colors-B8B_Y-lz.js";import{T as A}from"./trash-2-B9ibnt3n.js";/**
1
+ import{l as M,o as D,s as N,u as o,k as n,h as a,F as y,w as b,n as c,D as i,I as V,v as d,g as z,q as f,z as v,r as B,W as k,j as q,f as j}from"./index-DI8QgDXv.js";import{b as C,c as P,a as T}from"./api-2XP2yG1A.js";import{g as W}from"./squad-colors-B8B_Y-lz.js";import{T as A}from"./trash-2-C86P23n5.js";/**
2
2
  * @license lucide-vue-next v0.474.0 - ISC
3
3
  *
4
4
  * This source code is licensed under the ISC license.
@@ -1 +1 @@
1
- import{o as U,Q as q,s as z,u as r,k as n,h as t,n as d,D as c,H as V,T,O as D,M as P,j as x,F as $,w as j,q as p,z as g,v as l,g as Q,i as E,f as G}from"./index-DSAUDIwK.js";import{b as A,a as O}from"./api-DPUP4xY9.js";import{S as R}from"./search-DUqJxRNn.js";import{A as I}from"./arrow-left-BMJZvMdZ.js";import{T as J}from"./trash-2-B9ibnt3n.js";const K={class:"flex h-full"},W={class:"p-3 border-b border-border space-y-2"},X={class:"flex items-center gap-2"},Y={class:"relative"},Z={class:"flex gap-2"},ee={class:"flex-1"},te={class:"flex-1"},se={class:"flex-1 overflow-y-auto"},oe={key:0,class:"p-4 text-xs text-muted-foreground"},re={key:1,class:"flex flex-col items-center justify-center h-full p-6 text-center text-muted-foreground"},ne=["onClick"],ae={class:"flex-1 min-w-0"},le={class:"text-xs text-foreground line-clamp-2"},ie={class:"flex items-center gap-2 mt-1"},ue={class:"text-xs text-muted-foreground"},de={class:"text-xs text-muted-foreground"},ce=["onClick"],fe={key:2,class:"p-3 text-center"},ve={key:0,class:"flex-1 flex flex-col"},me={class:"flex items-center gap-2 px-4 py-2 border-b border-border"},xe={class:"text-sm font-medium text-muted-foreground"},pe={class:"flex-1 overflow-y-auto p-4 space-y-4"},ge={key:0,class:"text-center text-xs text-muted-foreground py-8"},he={class:"text-xs mt-1 opacity-60"},ye={key:1,class:"hidden md:flex flex-1 items-center justify-center text-muted-foreground"},be={class:"text-center"},_e=50,Le=U({__name:"HistoryView",setup(we){const a=l([]),h=l(0),y=l(!0),f=l(""),v=l(""),m=l(""),u=l(null),b=l([]),_=l(!1),w=l(0);async function k(o=!0){y.value=!0;try{o&&(w.value=0,a.value=[]);const e=new URLSearchParams;f.value&&e.set("q",f.value),v.value&&e.set("from",v.value),m.value&&e.set("to",m.value+"T23:59:59"),e.set("limit",String(_e)),e.set("offset",String(w.value));const i=await A(`/history?${e.toString()}`);a.value=o?i.items:[...a.value,...i.items],h.value=i.total,w.value+=i.items.length}finally{y.value=!1}}async function B(o){u.value=o,_.value=!0;try{b.value=await A(`/history/${o}`)}finally{_.value=!1}}function L(){u.value=null,b.value=[]}async function F(o,e){e.stopPropagation(),confirm("Delete this conversation?")&&(await O(`/history/${o}`),a.value=a.value.filter(i=>i.id!==o),h.value=Math.max(0,h.value-1),u.value===o&&L())}function C(o){return new Date(o).toLocaleString(void 0,{year:"numeric",month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}function H(o,e=100){return o.length>e?o.slice(0,e)+"…":o}const N=Q(()=>a.value.length<h.value);let S=null;return q([f,v,m],()=>{S&&clearTimeout(S),S=setTimeout(()=>k(!0),300)}),z(()=>k(!0)),(o,e)=>{var i;return r(),n("div",K,[t("div",{class:p(["flex flex-col border-r border-border",u.value?"hidden md:flex w-80 shrink-0":"flex-1"])},[t("div",W,[t("div",X,[d(c(V),{class:"w-4 h-4 text-muted-foreground"}),e[4]||(e[4]=t("span",{class:"text-sm font-medium"},"Conversation History",-1))]),t("div",Y,[d(c(R),{class:"absolute left-2.5 top-2.5 w-3.5 h-3.5 text-muted-foreground"}),T(t("input",{"onUpdate:modelValue":e[0]||(e[0]=s=>f.value=s),placeholder:"Search conversations...",class:"w-full rounded-md border border-input bg-background pl-8 pr-3 py-2 text-xs focus:outline-none focus:ring-1 focus:ring-ring"},null,512),[[D,f.value]])]),t("div",Z,[t("div",ee,[e[5]||(e[5]=t("label",{class:"text-xs text-muted-foreground block mb-1"},"From",-1)),T(t("input",{"onUpdate:modelValue":e[1]||(e[1]=s=>v.value=s),type:"date",class:"w-full rounded-md border border-input bg-background px-2 py-1.5 text-xs focus:outline-none focus:ring-1 focus:ring-ring"},null,512),[[D,v.value]])]),t("div",te,[e[6]||(e[6]=t("label",{class:"text-xs text-muted-foreground block mb-1"},"To",-1)),T(t("input",{"onUpdate:modelValue":e[2]||(e[2]=s=>m.value=s),type:"date",class:"w-full rounded-md border border-input bg-background px-2 py-1.5 text-xs focus:outline-none focus:ring-1 focus:ring-ring"},null,512),[[D,m.value]])])])]),t("div",se,[y.value&&a.value.length===0?(r(),n("div",oe," Loading... ")):a.value.length===0?(r(),n("div",re,[d(c(P),{class:"w-10 h-10 mb-3 opacity-40"}),e[7]||(e[7]=t("p",{class:"text-sm"},"No conversations found.",-1)),e[8]||(e[8]=t("p",{class:"text-xs mt-1"},"Start chatting to build up your history.",-1))])):x("",!0),(r(!0),n($,null,j(a.value,s=>(r(),n("div",{key:s.id,class:p(["group flex items-start gap-2 px-3 py-3 border-b border-border cursor-pointer hover:bg-accent/50 transition-colors",u.value===s.id?"bg-accent":""]),onClick:M=>B(s.id)},[t("div",ae,[t("p",le,g(H(s.preview)),1),t("div",ie,[t("span",ue,g(C(s.updatedAt)),1),t("span",de,"· "+g(s.messageCount)+" msgs",1)])]),t("button",{class:"opacity-0 group-hover:opacity-100 p-1 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive transition-all",title:"Delete",onClick:M=>F(s.id,M)},[d(c(J),{class:"w-3.5 h-3.5"})],8,ce)],10,ne))),128)),N.value?(r(),n("div",fe,[t("button",{class:"text-xs text-muted-foreground hover:text-foreground underline",onClick:e[3]||(e[3]=s=>k(!1))}," Load more ")])):x("",!0)])],2),u.value?(r(),n("div",ve,[t("div",me,[t("button",{class:"p-1.5 rounded hover:bg-accent text-muted-foreground",title:"Back",onClick:L},[d(c(I),{class:"w-4 h-4"})]),t("span",xe,g(C(((i=a.value.find(s=>s.id===u.value))==null?void 0:i.startedAt)??"")),1)]),t("div",pe,[_.value?(r(),n("div",ge," Loading... ")):x("",!0),(r(!0),n($,null,j(b.value,s=>(r(),n("div",{key:s.id,class:p(["flex",s.role==="user"?"justify-end":"justify-start"])},[t("div",{class:p(["max-w-[75%] rounded-lg px-4 py-2 text-sm",s.role==="user"?"bg-blue-600 text-white":"bg-muted text-foreground"])},[s.content?(r(),E(G,{key:0,content:s.content,class:p(s.role==="user"?"prose-invert":"")},null,8,["content","class"])):x("",!0),t("p",he,g(C(s.createdAt)),1)],2)],2))),128))])])):u.value?x("",!0):(r(),n("div",ye,[t("div",be,[d(c(V),{class:"w-12 h-12 mx-auto mb-3 opacity-50"}),e[9]||(e[9]=t("p",null,"Select a conversation to view",-1))])]))])}}});export{Le as default};
1
+ import{o as U,Q as q,s as z,u as r,k as n,h as t,n as d,D as c,H as V,T,O as D,M as P,j as x,F as $,w as j,q as p,z as g,v as l,g as Q,i as E,f as G}from"./index-DI8QgDXv.js";import{b as A,a as O}from"./api-2XP2yG1A.js";import{S as R}from"./search-BnsxENnH.js";import{A as I}from"./arrow-left-BsbWSKAE.js";import{T as J}from"./trash-2-C86P23n5.js";const K={class:"flex h-full"},W={class:"p-3 border-b border-border space-y-2"},X={class:"flex items-center gap-2"},Y={class:"relative"},Z={class:"flex gap-2"},ee={class:"flex-1"},te={class:"flex-1"},se={class:"flex-1 overflow-y-auto"},oe={key:0,class:"p-4 text-xs text-muted-foreground"},re={key:1,class:"flex flex-col items-center justify-center h-full p-6 text-center text-muted-foreground"},ne=["onClick"],ae={class:"flex-1 min-w-0"},le={class:"text-xs text-foreground line-clamp-2"},ie={class:"flex items-center gap-2 mt-1"},ue={class:"text-xs text-muted-foreground"},de={class:"text-xs text-muted-foreground"},ce=["onClick"],fe={key:2,class:"p-3 text-center"},ve={key:0,class:"flex-1 flex flex-col"},me={class:"flex items-center gap-2 px-4 py-2 border-b border-border"},xe={class:"text-sm font-medium text-muted-foreground"},pe={class:"flex-1 overflow-y-auto p-4 space-y-4"},ge={key:0,class:"text-center text-xs text-muted-foreground py-8"},he={class:"text-xs mt-1 opacity-60"},ye={key:1,class:"hidden md:flex flex-1 items-center justify-center text-muted-foreground"},be={class:"text-center"},_e=50,Le=U({__name:"HistoryView",setup(we){const a=l([]),h=l(0),y=l(!0),f=l(""),v=l(""),m=l(""),u=l(null),b=l([]),_=l(!1),w=l(0);async function k(o=!0){y.value=!0;try{o&&(w.value=0,a.value=[]);const e=new URLSearchParams;f.value&&e.set("q",f.value),v.value&&e.set("from",v.value),m.value&&e.set("to",m.value+"T23:59:59"),e.set("limit",String(_e)),e.set("offset",String(w.value));const i=await A(`/history?${e.toString()}`);a.value=o?i.items:[...a.value,...i.items],h.value=i.total,w.value+=i.items.length}finally{y.value=!1}}async function B(o){u.value=o,_.value=!0;try{b.value=await A(`/history/${o}`)}finally{_.value=!1}}function L(){u.value=null,b.value=[]}async function F(o,e){e.stopPropagation(),confirm("Delete this conversation?")&&(await O(`/history/${o}`),a.value=a.value.filter(i=>i.id!==o),h.value=Math.max(0,h.value-1),u.value===o&&L())}function C(o){return new Date(o).toLocaleString(void 0,{year:"numeric",month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}function H(o,e=100){return o.length>e?o.slice(0,e)+"…":o}const N=Q(()=>a.value.length<h.value);let S=null;return q([f,v,m],()=>{S&&clearTimeout(S),S=setTimeout(()=>k(!0),300)}),z(()=>k(!0)),(o,e)=>{var i;return r(),n("div",K,[t("div",{class:p(["flex flex-col border-r border-border",u.value?"hidden md:flex w-80 shrink-0":"flex-1"])},[t("div",W,[t("div",X,[d(c(V),{class:"w-4 h-4 text-muted-foreground"}),e[4]||(e[4]=t("span",{class:"text-sm font-medium"},"Conversation History",-1))]),t("div",Y,[d(c(R),{class:"absolute left-2.5 top-2.5 w-3.5 h-3.5 text-muted-foreground"}),T(t("input",{"onUpdate:modelValue":e[0]||(e[0]=s=>f.value=s),placeholder:"Search conversations...",class:"w-full rounded-md border border-input bg-background pl-8 pr-3 py-2 text-xs focus:outline-none focus:ring-1 focus:ring-ring"},null,512),[[D,f.value]])]),t("div",Z,[t("div",ee,[e[5]||(e[5]=t("label",{class:"text-xs text-muted-foreground block mb-1"},"From",-1)),T(t("input",{"onUpdate:modelValue":e[1]||(e[1]=s=>v.value=s),type:"date",class:"w-full rounded-md border border-input bg-background px-2 py-1.5 text-xs focus:outline-none focus:ring-1 focus:ring-ring"},null,512),[[D,v.value]])]),t("div",te,[e[6]||(e[6]=t("label",{class:"text-xs text-muted-foreground block mb-1"},"To",-1)),T(t("input",{"onUpdate:modelValue":e[2]||(e[2]=s=>m.value=s),type:"date",class:"w-full rounded-md border border-input bg-background px-2 py-1.5 text-xs focus:outline-none focus:ring-1 focus:ring-ring"},null,512),[[D,m.value]])])])]),t("div",se,[y.value&&a.value.length===0?(r(),n("div",oe," Loading... ")):a.value.length===0?(r(),n("div",re,[d(c(P),{class:"w-10 h-10 mb-3 opacity-40"}),e[7]||(e[7]=t("p",{class:"text-sm"},"No conversations found.",-1)),e[8]||(e[8]=t("p",{class:"text-xs mt-1"},"Start chatting to build up your history.",-1))])):x("",!0),(r(!0),n($,null,j(a.value,s=>(r(),n("div",{key:s.id,class:p(["group flex items-start gap-2 px-3 py-3 border-b border-border cursor-pointer hover:bg-accent/50 transition-colors",u.value===s.id?"bg-accent":""]),onClick:M=>B(s.id)},[t("div",ae,[t("p",le,g(H(s.preview)),1),t("div",ie,[t("span",ue,g(C(s.updatedAt)),1),t("span",de,"· "+g(s.messageCount)+" msgs",1)])]),t("button",{class:"opacity-0 group-hover:opacity-100 p-1 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive transition-all",title:"Delete",onClick:M=>F(s.id,M)},[d(c(J),{class:"w-3.5 h-3.5"})],8,ce)],10,ne))),128)),N.value?(r(),n("div",fe,[t("button",{class:"text-xs text-muted-foreground hover:text-foreground underline",onClick:e[3]||(e[3]=s=>k(!1))}," Load more ")])):x("",!0)])],2),u.value?(r(),n("div",ve,[t("div",me,[t("button",{class:"p-1.5 rounded hover:bg-accent text-muted-foreground",title:"Back",onClick:L},[d(c(I),{class:"w-4 h-4"})]),t("span",xe,g(C(((i=a.value.find(s=>s.id===u.value))==null?void 0:i.startedAt)??"")),1)]),t("div",pe,[_.value?(r(),n("div",ge," Loading... ")):x("",!0),(r(!0),n($,null,j(b.value,s=>(r(),n("div",{key:s.id,class:p(["flex",s.role==="user"?"justify-end":"justify-start"])},[t("div",{class:p(["max-w-[75%] rounded-lg px-4 py-2 text-sm",s.role==="user"?"bg-blue-600 text-white":"bg-muted text-foreground"])},[s.content?(r(),E(G,{key:0,content:s.content,class:p(s.role==="user"?"prose-invert":"")},null,8,["content","class"])):x("",!0),t("p",he,g(C(s.createdAt)),1)],2)],2))),128))])])):u.value?x("",!0):(r(),n("div",ye,[t("div",be,[d(c(V),{class:"w-12 h-12 mx-auto mb-3 opacity-50"}),e[9]||(e[9]=t("p",null,"Select a conversation to view",-1))])]))])}}});export{Le as default};
@@ -1 +1 @@
1
- import{o as b,E as v,u,k as i,h as t,n as y,W as w,T as m,O as p,z as f,j as h,D as g,v as d,_,K as S}from"./index-DSAUDIwK.js";const V={class:"min-h-screen flex items-center justify-center bg-background p-4"},k={class:"w-full max-w-sm space-y-8"},D={class:"text-center"},A={key:0,class:"text-sm text-destructive"},B=["disabled"],L=b({__name:"LoginView",setup(E){const o=v(),c=S(),r=d(""),n=d(""),s=d("");async function x(){s.value="";try{await o.login(r.value,n.value),c.push("/")}catch(l){s.value=l.message??"Login failed"}}return(l,e)=>(u(),i("div",V,[t("div",k,[t("div",D,[y(_,{size:56,class:"mx-auto mb-4"}),e[2]||(e[2]=t("h1",{class:"text-2xl font-bold bg-gradient-brand bg-clip-text text-transparent"}," IO ",-1)),e[3]||(e[3]=t("p",{class:"text-sm text-muted-foreground mt-1"},"Sign in to your dashboard",-1))]),t("form",{onSubmit:w(x,["prevent"]),class:"space-y-4 bg-card border border-border rounded-lg p-6"},[t("div",null,[e[4]||(e[4]=t("label",{class:"text-sm font-medium text-muted-foreground",for:"email"},"Email",-1)),m(t("input",{id:"email","onUpdate:modelValue":e[0]||(e[0]=a=>r.value=a),type:"email",required:"",class:"mt-1 w-full rounded-md border border-border bg-input px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring",placeholder:"you@example.com"},null,512),[[p,r.value]])]),t("div",null,[e[5]||(e[5]=t("label",{class:"text-sm font-medium text-muted-foreground",for:"password"},"Password",-1)),m(t("input",{id:"password","onUpdate:modelValue":e[1]||(e[1]=a=>n.value=a),type:"password",required:"",class:"mt-1 w-full rounded-md border border-border bg-input px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring",placeholder:"••••••••"},null,512),[[p,n.value]])]),s.value?(u(),i("div",A,f(s.value),1)):h("",!0),t("button",{type:"submit",disabled:g(o).loading,class:"btn-gradient w-full py-2.5"},f(g(o).loading?"Signing in...":"Sign In"),9,B)],32),e[6]||(e[6]=t("p",{class:"text-center text-xs text-muted-foreground"}," Personal AI Assistant Daemon ",-1))])]))}});export{L as default};
1
+ import{o as b,E as v,u,k as i,h as t,n as y,W as w,T as m,O as p,z as f,j as h,D as g,v as d,_,K as S}from"./index-DI8QgDXv.js";const V={class:"min-h-screen flex items-center justify-center bg-background p-4"},k={class:"w-full max-w-sm space-y-8"},D={class:"text-center"},A={key:0,class:"text-sm text-destructive"},B=["disabled"],L=b({__name:"LoginView",setup(E){const o=v(),c=S(),r=d(""),n=d(""),s=d("");async function x(){s.value="";try{await o.login(r.value,n.value),c.push("/")}catch(l){s.value=l.message??"Login failed"}}return(l,e)=>(u(),i("div",V,[t("div",k,[t("div",D,[y(_,{size:56,class:"mx-auto mb-4"}),e[2]||(e[2]=t("h1",{class:"text-2xl font-bold bg-gradient-brand bg-clip-text text-transparent"}," IO ",-1)),e[3]||(e[3]=t("p",{class:"text-sm text-muted-foreground mt-1"},"Sign in to your dashboard",-1))]),t("form",{onSubmit:w(x,["prevent"]),class:"space-y-4 bg-card border border-border rounded-lg p-6"},[t("div",null,[e[4]||(e[4]=t("label",{class:"text-sm font-medium text-muted-foreground",for:"email"},"Email",-1)),m(t("input",{id:"email","onUpdate:modelValue":e[0]||(e[0]=a=>r.value=a),type:"email",required:"",class:"mt-1 w-full rounded-md border border-border bg-input px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring",placeholder:"you@example.com"},null,512),[[p,r.value]])]),t("div",null,[e[5]||(e[5]=t("label",{class:"text-sm font-medium text-muted-foreground",for:"password"},"Password",-1)),m(t("input",{id:"password","onUpdate:modelValue":e[1]||(e[1]=a=>n.value=a),type:"password",required:"",class:"mt-1 w-full rounded-md border border-border bg-input px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring",placeholder:"••••••••"},null,512),[[p,n.value]])]),s.value?(u(),i("div",A,f(s.value),1)):h("",!0),t("button",{type:"submit",disabled:g(o).loading,class:"btn-gradient w-full py-2.5"},f(g(o).loading?"Signing in...":"Sign In"),9,B)],32),e[6]||(e[6]=t("p",{class:"text-center text-xs text-muted-foreground"}," Personal AI Assistant Daemon ",-1))])]))}});export{L as default};