@lightupai/polaris 0.0.79 → 0.0.80
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.
- package/.github/workflows/cd.yml +37 -2
- package/docker-compose.prod.yml +1 -0
- package/docs/signup-flow.md +10 -9
- package/package.json +1 -1
- package/src/client/client.ts +6 -6
- package/src/daemon/daemon.ts +13 -36
- package/src/web/app.ts +27 -5
- package/src/web/views.ts +32 -11
package/.github/workflows/cd.yml
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
name: CD
|
|
2
2
|
|
|
3
|
-
# Continuous deployment: on every push (merge) to master
|
|
4
|
-
# host and run deploy.sh (git pull + docker compose build/up).
|
|
3
|
+
# Continuous deployment: on every push (merge) to master:
|
|
4
|
+
# 1. SSH to the prod host and run deploy.sh (git pull + docker compose build/up).
|
|
5
|
+
# 2. Publish @lightupai/polaris to npm if the version in package.json has been
|
|
6
|
+
# bumped beyond what is already published (i.e. only when there are client-side
|
|
7
|
+
# changes that warrant a release).
|
|
5
8
|
#
|
|
6
9
|
# Required repository secrets (Settings → Secrets and variables → Actions):
|
|
7
10
|
# DEPLOY_HOST — hostname or IP of the prod box (e.g. polaris.lightup.ai)
|
|
8
11
|
# DEPLOY_SSH_KEY — private SSH key (OpenSSH format) for the deploy user
|
|
12
|
+
# NPM_TOKEN — npm Automation token (bypasses OTP; create at npmjs.com)
|
|
9
13
|
#
|
|
10
14
|
# Optional repository secrets (deploy.sh has sensible defaults):
|
|
11
15
|
# DEPLOY_USER — SSH user on the prod box (default: deploy)
|
|
@@ -75,3 +79,34 @@ jobs:
|
|
|
75
79
|
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
|
|
76
80
|
DEPLOY_DIR: ${{ secrets.DEPLOY_DIR }}
|
|
77
81
|
run: ./deploy.sh
|
|
82
|
+
|
|
83
|
+
publish-npm:
|
|
84
|
+
runs-on: ubuntu-latest
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/checkout@v4
|
|
87
|
+
|
|
88
|
+
- uses: actions/setup-node@v4
|
|
89
|
+
with:
|
|
90
|
+
node-version: '20'
|
|
91
|
+
registry-url: 'https://registry.npmjs.org'
|
|
92
|
+
|
|
93
|
+
- name: Check if version needs publishing
|
|
94
|
+
id: version-check
|
|
95
|
+
run: |
|
|
96
|
+
LOCAL=$(node -p "require('./package.json').version")
|
|
97
|
+
PUBLISHED=$(npm view @lightupai/polaris version 2>/dev/null || echo "0.0.0")
|
|
98
|
+
echo "local=$LOCAL" >> "$GITHUB_OUTPUT"
|
|
99
|
+
echo "published=$PUBLISHED" >> "$GITHUB_OUTPUT"
|
|
100
|
+
if [ "$LOCAL" != "$PUBLISHED" ]; then
|
|
101
|
+
echo "should_publish=true" >> "$GITHUB_OUTPUT"
|
|
102
|
+
echo "Version $LOCAL differs from published $PUBLISHED — will publish."
|
|
103
|
+
else
|
|
104
|
+
echo "should_publish=false" >> "$GITHUB_OUTPUT"
|
|
105
|
+
echo "Version $LOCAL already published — skipping npm publish."
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
- name: Publish to npm
|
|
109
|
+
if: steps.version-check.outputs.should_publish == 'true'
|
|
110
|
+
env:
|
|
111
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
112
|
+
run: npm publish --access public
|
package/docker-compose.prod.yml
CHANGED
|
@@ -46,6 +46,7 @@ services:
|
|
|
46
46
|
SLACK_CLIENT_SECRET: ${SLACK_CLIENT_SECRET}
|
|
47
47
|
SLACK_REDIRECT_URI: https://app.withpolaris.ai/slack/callback
|
|
48
48
|
SIGNUP_SLACK_BOT_TOKEN: ${SIGNUP_SLACK_BOT_TOKEN:-}
|
|
49
|
+
POLARIS_API_URL: http://api:4321
|
|
49
50
|
depends_on:
|
|
50
51
|
postgres:
|
|
51
52
|
condition: service_healthy
|
package/docs/signup-flow.md
CHANGED
|
@@ -10,9 +10,9 @@ A user who signs up should experience a smooth path from landing page to their f
|
|
|
10
10
|
|
|
11
11
|
1. Choose a plan (or default to free)
|
|
12
12
|
2. Sign up with Google
|
|
13
|
-
3. Connect their Slack workspace
|
|
13
|
+
3. Connect their Slack workspace *(optional — can be skipped and done later)*
|
|
14
14
|
4. Install the CLI
|
|
15
|
-
5.
|
|
15
|
+
5. Start their first session (join a channel if Slack is connected)
|
|
16
16
|
|
|
17
17
|
Each step should feel intentional, not accidental. The user should never be left wondering "what do I do next?"
|
|
18
18
|
|
|
@@ -34,17 +34,18 @@ After Google OAuth completes, the callback handler should route based on the use
|
|
|
34
34
|
|---|---|
|
|
35
35
|
| Existing user (login) | Dashboard |
|
|
36
36
|
| New user, existing org | Dashboard (org already set up) |
|
|
37
|
-
| New user, new org, no Slack | Onboarding: Connect Slack |
|
|
37
|
+
| New user, new org, no Slack | Onboarding: Connect Slack (with skip option) |
|
|
38
38
|
| New user, new org, Slack connected | Onboarding: Install CLI |
|
|
39
39
|
|
|
40
40
|
### Onboarding Flow
|
|
41
41
|
|
|
42
42
|
A dedicated onboarding page replaces the empty dashboard for new orgs. Three steps, shown as a checklist:
|
|
43
43
|
|
|
44
|
-
**Step 1: Connect Slack**
|
|
45
|
-
- Large "
|
|
46
|
-
- Explain: "
|
|
47
|
-
-
|
|
44
|
+
**Step 1: Connect Slack** *(optional)*
|
|
45
|
+
- Large "Connect Slack" button
|
|
46
|
+
- Explain: "Share sessions with your team in real time. Connect Slack to publish your session log to a channel."
|
|
47
|
+
- "Skip for now" link lets users proceed without connecting. Skip state is stored in a browser cookie (`polaris_floor_skipped`).
|
|
48
|
+
- After connecting (or skipping), advance to Step 2
|
|
48
49
|
|
|
49
50
|
**Step 2: Install the CLI**
|
|
50
51
|
- Show `npx @lightupai/polaris` with a copy button
|
|
@@ -120,8 +121,8 @@ When a free-tier org approaches or hits their limit:
|
|
|
120
121
|
- [ ] **Store selected plan on org record**
|
|
121
122
|
Add a `plan` column to the orgs table (free/team/enterprise, default 'free'). Populate from the `?plan` query param during signup. Show current plan on the dashboard.
|
|
122
123
|
|
|
123
|
-
- [
|
|
124
|
-
|
|
124
|
+
- [x] **Prompt Slack connection immediately after signup**
|
|
125
|
+
Setup view shows Floor as step 1 with a "Connect Slack" button. Users can skip via "Skip for now" (cookie-based) and proceed to install the CLI and start sessions without Slack. Floor can be connected at any time from the setup or active dashboard.
|
|
125
126
|
|
|
126
127
|
- [ ] **Post-signup onboarding flow**
|
|
127
128
|
After a brand new org signup, redirect to onboarding page (not dashboard). Three steps: Connect Slack → Install CLI → Join a channel. Repurpose existing `renderWelcomePage`/`renderSetupView`. Auto-advance when each step is completed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightupai/polaris",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.80",
|
|
4
4
|
"description": "Record and stream Claude Code sessions to Slack. Like Gong for AI coding — capture every prompt, response, and tool call. Multiplayer collaboration across AI coding agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
package/src/client/client.ts
CHANGED
|
@@ -71,7 +71,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
71
71
|
inputSchema: {
|
|
72
72
|
type: "object" as const,
|
|
73
73
|
properties: {
|
|
74
|
-
channel: { type: "string", description: "
|
|
74
|
+
channel: { type: "string", description: "Project workspace to join (e.g., #my-project). Becomes the Slack channel name if a floor is connected. Omit to list existing projects." },
|
|
75
75
|
user: { type: "string", description: "Your participant ID (e.g., user:manu)" },
|
|
76
76
|
session: { type: "string", description: "Session name (optional — auto-generated if omitted)" },
|
|
77
77
|
agent: { type: "string", description: "Agent identity (optional — defaults to agent:claude)" },
|
|
@@ -97,7 +97,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
97
97
|
},
|
|
98
98
|
{
|
|
99
99
|
name: "polaris_reply",
|
|
100
|
-
description: "Send a message to the project floor (visible to all advisors and the Slack
|
|
100
|
+
description: "Send a message to the project floor (visible to all advisors, and to the linked Slack channel if a floor is connected).",
|
|
101
101
|
inputSchema: {
|
|
102
102
|
type: "object" as const,
|
|
103
103
|
properties: {
|
|
@@ -108,7 +108,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
108
108
|
},
|
|
109
109
|
{
|
|
110
110
|
name: "polaris_rename",
|
|
111
|
-
description: "Rename the current project. Also renames the Slack channel.",
|
|
111
|
+
description: "Rename the current project. Also renames the linked Slack channel if a floor is connected.",
|
|
112
112
|
inputSchema: {
|
|
113
113
|
type: "object" as const,
|
|
114
114
|
properties: {
|
|
@@ -184,12 +184,12 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
184
184
|
if (res.ok) {
|
|
185
185
|
const body = await res.json() as { channels: string[] };
|
|
186
186
|
if (body.channels.length === 0) {
|
|
187
|
-
return { content: [{ type: "text", text: "No
|
|
187
|
+
return { content: [{ type: "text", text: "No projects found. Start one with: `/polaris join #my-project`" }] };
|
|
188
188
|
}
|
|
189
|
-
return { content: [{ type: "text", text: `Available
|
|
189
|
+
return { content: [{ type: "text", text: `Available projects:\n${body.channels.map(c => ` ${c}`).join("\n")}\n\nJoin one with: /polaris join #project-name` }] };
|
|
190
190
|
}
|
|
191
191
|
} catch { /* fall through */ }
|
|
192
|
-
return { content: [{ type: "text", text: "Specify a
|
|
192
|
+
return { content: [{ type: "text", text: "Specify a project: `/polaris join #my-project`" }] };
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
const project = channel.replace(/^#/, ""); // strip leading # if present
|
package/src/daemon/daemon.ts
CHANGED
|
@@ -13,7 +13,7 @@ interface SessionMapping {
|
|
|
13
13
|
agent: string;
|
|
14
14
|
slackChannel?: string;
|
|
15
15
|
ws: WebSocket | null;
|
|
16
|
-
pendingMapping?: boolean; // true until
|
|
16
|
+
pendingMapping?: boolean; // true until the first hook event from this shell claims the mapping
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
function generateSessionName(): string {
|
|
@@ -590,19 +590,18 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
590
590
|
// MCP, connected via /connect, or a previously learned alias) —
|
|
591
591
|
// always routed by that match, regardless of how many other
|
|
592
592
|
// sessions are connected. A matchable event is never dropped.
|
|
593
|
-
// 2. No match +
|
|
594
|
-
//
|
|
595
|
-
//
|
|
596
|
-
//
|
|
597
|
-
// 3. No match +
|
|
598
|
-
//
|
|
599
|
-
//
|
|
593
|
+
// 2. No exact match + a pending session exists: the MCP client's
|
|
594
|
+
// generated UUID differs from CC's hook session_id. The first
|
|
595
|
+
// hook event from the shell that ran /connect claims the pending
|
|
596
|
+
// slot and becomes the canonical alias going forward.
|
|
597
|
+
// 3. No match + no pending session: unknown hook — the shell never
|
|
598
|
+
// ran polaris_connect. Always returns not_connected; hooks from
|
|
599
|
+
// unjoined shells are never auto-routed to an existing session.
|
|
600
600
|
let mapping = sessions.get(ccSessionId);
|
|
601
601
|
if (!mapping || !mapping.project) {
|
|
602
602
|
// CC session_id doesn't match any registered MCP client. The MCP
|
|
603
603
|
// server uses a different UUID than CC's session_id. Match to a
|
|
604
|
-
// session with pendingMapping (most recent first)
|
|
605
|
-
// single-connected-session heuristic when none is pending.
|
|
604
|
+
// session with pendingMapping (most recent first).
|
|
606
605
|
const pending = Array.from(sessions.values()).filter((m) => m.project && m.pendingMapping);
|
|
607
606
|
if (pending.length > 0) {
|
|
608
607
|
// Map the CC session ID to the most recently connected pending session
|
|
@@ -618,32 +617,10 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
618
617
|
injectQueues.set(mapping.ccSessionId, queue);
|
|
619
618
|
injectQueues.set(ccSessionId, queue);
|
|
620
619
|
} else {
|
|
621
|
-
// No pending sessions —
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
mapping = connectedSessions[0];
|
|
626
|
-
sessions.set(ccSessionId, { ...mapping, ccSessionId, slackChannel: undefined });
|
|
627
|
-
// Share the inject queue (see note above).
|
|
628
|
-
const queue = injectQueues.get(mapping.ccSessionId) ?? [];
|
|
629
|
-
injectQueues.set(mapping.ccSessionId, queue);
|
|
630
|
-
injectQueues.set(ccSessionId, queue);
|
|
631
|
-
} else if (connectedSessions.length > 1) {
|
|
632
|
-
// Multiple sessions and session_id matches none of them (nor any
|
|
633
|
-
// learned alias) — can't determine which one. Drop with a clear
|
|
634
|
-
// warning, never silently.
|
|
635
|
-
const candidates = connectedSessions.map((m) => `${m.project}/${m.session}`).join(", ");
|
|
636
|
-
console.error(
|
|
637
|
-
`polaris daemon: dropping ${String(body.hook_event_name ?? "hook")} event — ` +
|
|
638
|
-
`session_id ${ccSessionId} matches no known mapping and ${connectedSessions.length} ` +
|
|
639
|
-
`sessions are connected (${candidates}). ` +
|
|
640
|
-
`Reconnect with polaris_connect in the affected Claude session to re-establish routing.`
|
|
641
|
-
);
|
|
642
|
-
await logEvent("/events", body, { status: 0, body: `dropped: ambiguous across ${connectedSessions.length} sessions (${candidates})` });
|
|
643
|
-
return json({ status: "ambiguous" });
|
|
644
|
-
} else {
|
|
645
|
-
return json({ status: "not_connected" });
|
|
646
|
-
}
|
|
620
|
+
// No pending sessions — this hook's session_id doesn't match any
|
|
621
|
+
// known mapping. Unjoined shells are never auto-routed; the user
|
|
622
|
+
// must run polaris_connect explicitly in this Claude session.
|
|
623
|
+
return json({ status: "not_connected" });
|
|
647
624
|
}
|
|
648
625
|
}
|
|
649
626
|
|
package/src/web/app.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
+
import { getCookie, setCookie } from "hono/cookie";
|
|
2
3
|
import { Google } from "arctic";
|
|
3
4
|
import { createToken, verifyToken } from "../service/auth";
|
|
4
5
|
import {
|
|
@@ -384,6 +385,8 @@ export function createApp(sql: Sql) {
|
|
|
384
385
|
|
|
385
386
|
const hasConnectedSession = allSessions.length > 0;
|
|
386
387
|
|
|
388
|
+
const floorSkipped = getCookie(c, "polaris_floor_skipped") === "1";
|
|
389
|
+
|
|
387
390
|
const ctx = {
|
|
388
391
|
token,
|
|
389
392
|
userName: payload.name,
|
|
@@ -391,6 +394,7 @@ export function createApp(sql: Sql) {
|
|
|
391
394
|
orgSlug: org.slug,
|
|
392
395
|
email: payload.email,
|
|
393
396
|
slackConnected: !!org.slack_team_id,
|
|
397
|
+
floorSkipped,
|
|
394
398
|
cliInstalled,
|
|
395
399
|
hasConnectedSession,
|
|
396
400
|
totalPrompts: Array.from(promptCounts.values()).reduce((a, b) => a + b, 0),
|
|
@@ -405,6 +409,15 @@ export function createApp(sql: Sql) {
|
|
|
405
409
|
return layout(renderSetupView(ctx, devices), "Setup — Polaris by Lightup");
|
|
406
410
|
});
|
|
407
411
|
|
|
412
|
+
// --- Skip floor setup ---
|
|
413
|
+
|
|
414
|
+
app.get("/skip/floor", async (c) => {
|
|
415
|
+
const token = c.req.query("token");
|
|
416
|
+
if (!token) return c.redirect("/login");
|
|
417
|
+
setCookie(c, "polaris_floor_skipped", "1", { path: "/", maxAge: 60 * 60 * 24 * 365, httpOnly: true, sameSite: "Lax" });
|
|
418
|
+
return c.redirect(`/dashboard?token=${token}`);
|
|
419
|
+
});
|
|
420
|
+
|
|
408
421
|
// --- Profile ---
|
|
409
422
|
|
|
410
423
|
app.get("/profile", async (c) => {
|
|
@@ -789,7 +802,7 @@ export function createApp(sql: Sql) {
|
|
|
789
802
|
|
|
790
803
|
app.get("/preview", (c) => {
|
|
791
804
|
const mockToken = "preview-token";
|
|
792
|
-
const base = { token: mockToken, userName: mockUser.name, orgName: mockOrg.name, orgSlug: "lightup-data" as string | null, email: mockUser.email };
|
|
805
|
+
const base = { token: mockToken, userName: mockUser.name, orgName: mockOrg.name, orgSlug: "lightup-data" as string | null, email: mockUser.email, floorSkipped: false };
|
|
793
806
|
|
|
794
807
|
const mockTeam = [{ name: mockUser.name, email: mockUser.email }, { name: "Alice Chen", email: "alice@lightup.ai" }, { name: "Laura Mowry", email: "laura@lightup.ai" }];
|
|
795
808
|
const mockSenders = ["user:manu.bansal", "user:alice.chen", "user:laura.mowry"];
|
|
@@ -799,10 +812,11 @@ export function createApp(sql: Sql) {
|
|
|
799
812
|
return { date: d.toISOString().slice(0, 10), sender, count: Math.floor(Math.random() * 12) + 1 };
|
|
800
813
|
})
|
|
801
814
|
);
|
|
802
|
-
const fresh
|
|
803
|
-
const
|
|
804
|
-
const
|
|
805
|
-
const
|
|
815
|
+
const fresh = { ...base, orgSlug: null, slackConnected: false, floorSkipped: false, cliInstalled: false, hasConnectedSession: false, totalPrompts: 0 };
|
|
816
|
+
const floorSkipped = { ...base, orgSlug: null, slackConnected: false, floorSkipped: true, cliInstalled: false, hasConnectedSession: false, totalPrompts: 0 };
|
|
817
|
+
const slackDone = { ...base, slackConnected: true, floorSkipped: false, cliInstalled: false, hasConnectedSession: false, totalPrompts: 0, teamMembers: mockTeam, dailyPrompts: mockDailyPrompts };
|
|
818
|
+
const cliDone = { ...base, slackConnected: true, floorSkipped: false, cliInstalled: true, hasConnectedSession: false, totalPrompts: 0, teamMembers: mockTeam, dailyPrompts: mockDailyPrompts };
|
|
819
|
+
const allDone = { ...base, slackConnected: true, floorSkipped: false, cliInstalled: true, hasConnectedSession: true, totalPrompts: 127, teamMembers: mockTeam, dailyPrompts: mockDailyPrompts };
|
|
806
820
|
const teamPlan = { ...fresh, plan: "pro" };
|
|
807
821
|
|
|
808
822
|
return layout(`
|
|
@@ -819,6 +833,14 @@ export function createApp(sql: Sql) {
|
|
|
819
833
|
</div>
|
|
820
834
|
</section>
|
|
821
835
|
|
|
836
|
+
<section>
|
|
837
|
+
<h2 class="text-lg font-bold text-gray-700 mb-1">Setup: floor skipped</h2>
|
|
838
|
+
<p class="text-sm text-gray-400 mb-4">User clicked "Skip for now" — floor subdued, Devices becomes active.</p>
|
|
839
|
+
<div class="border border-gray-200 rounded-xl overflow-hidden shadow-sm">
|
|
840
|
+
${renderSetupView(floorSkipped)}
|
|
841
|
+
</div>
|
|
842
|
+
</section>
|
|
843
|
+
|
|
822
844
|
<section>
|
|
823
845
|
<h2 class="text-lg font-bold text-gray-700 mb-1">Setup: Slack connected</h2>
|
|
824
846
|
<p class="text-sm text-gray-400 mb-4">Floor is live, CLI not installed yet.</p>
|
package/src/web/views.ts
CHANGED
|
@@ -17,6 +17,7 @@ interface ViewContext {
|
|
|
17
17
|
orgSlug: string | null;
|
|
18
18
|
email: string;
|
|
19
19
|
slackConnected: boolean;
|
|
20
|
+
floorSkipped: boolean;
|
|
20
21
|
cliInstalled: boolean;
|
|
21
22
|
hasConnectedSession: boolean;
|
|
22
23
|
totalPrompts: number;
|
|
@@ -276,26 +277,42 @@ function renderFloorSection(ctx: ViewContext, compact = false, state: StepState
|
|
|
276
277
|
</div>`;
|
|
277
278
|
}
|
|
278
279
|
|
|
280
|
+
if (ctx.floorSkipped) {
|
|
281
|
+
return `
|
|
282
|
+
<div class="opacity-50">
|
|
283
|
+
<div class="flex items-baseline gap-2 mb-3">
|
|
284
|
+
<h2 class="text-xs font-semibold text-gray-400 uppercase tracking-wider">Floor</h2>
|
|
285
|
+
${statusBadge("Skipped", false)}
|
|
286
|
+
</div>
|
|
287
|
+
<div class="bg-gray-50 border border-gray-200 rounded-lg px-5 py-3 flex items-center justify-between">
|
|
288
|
+
<p class="text-sm text-gray-400">No floor connected.</p>
|
|
289
|
+
<a href="/slack/install?token=${ctx.token}" class="text-xs text-gray-400 hover:text-gray-600 transition">Connect Slack</a>
|
|
290
|
+
</div>
|
|
291
|
+
</div>`;
|
|
292
|
+
}
|
|
293
|
+
|
|
279
294
|
return sectionWrap(state, `
|
|
280
295
|
<div>
|
|
281
296
|
<div class="flex items-baseline gap-2 mb-3">
|
|
282
|
-
<h2 class="text-xs font-semibold text-gray-400 uppercase tracking-wider">Floor</h2>
|
|
283
|
-
${statusBadge("Not connected", false)}
|
|
297
|
+
<h2 class="text-xs font-semibold text-gray-400 uppercase tracking-wider">Floor <span class="text-[10px] font-normal normal-case text-gray-400">— optional</span></h2>
|
|
284
298
|
</div>
|
|
285
|
-
<div class="bg-white border ${state === "active" ? cardClass("active") : "border-
|
|
299
|
+
<div class="bg-white border ${state === "active" ? cardClass("active") : "border-gray-200"} rounded-lg p-5">
|
|
286
300
|
<div class="flex items-center gap-3">
|
|
287
301
|
<div class="w-10 h-10 rounded-lg bg-gray-100 flex items-center justify-center shrink-0">
|
|
288
302
|
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
|
|
289
303
|
</div>
|
|
290
304
|
<div>
|
|
291
|
-
<p class="text-sm font-semibold text-gray-900">
|
|
292
|
-
<p class="text-sm text-gray-500 mt-0.5">
|
|
305
|
+
<p class="text-sm font-semibold text-gray-900">Connect a floor</p>
|
|
306
|
+
<p class="text-sm text-gray-500 mt-0.5">Share sessions with your team in real time. Connect Slack to publish your session log to a channel.</p>
|
|
293
307
|
</div>
|
|
294
308
|
</div>
|
|
295
|
-
<
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
309
|
+
<div class="mt-3 flex items-center gap-4">
|
|
310
|
+
<a href="/slack/install?token=${ctx.token}" class="inline-flex items-center gap-2 px-4 py-2 bg-[#4A154B] text-white text-sm font-medium rounded-lg hover:bg-[#3a1039] transition">
|
|
311
|
+
${slackIcon}
|
|
312
|
+
Connect Slack
|
|
313
|
+
</a>
|
|
314
|
+
<a href="/skip/floor?token=${ctx.token}" class="text-sm text-gray-400 hover:text-gray-600 transition">Skip for now</a>
|
|
315
|
+
</div>
|
|
299
316
|
</div>
|
|
300
317
|
</div>`);
|
|
301
318
|
}
|
|
@@ -402,7 +419,10 @@ function renderProjectsSessionsSection(ctx: ViewContext, sessions: SessionFixtur
|
|
|
402
419
|
: "Inside your AI agent (Claude Code, Cursor, etc.), run:"}</p>
|
|
403
420
|
${ctx.hasConnectedSession
|
|
404
421
|
? ""
|
|
405
|
-
: copyBlock("/polaris join #my-
|
|
422
|
+
: copyBlock("/polaris join #my-project")}
|
|
423
|
+
${ctx.hasConnectedSession
|
|
424
|
+
? ""
|
|
425
|
+
: `<p class="text-xs text-gray-400 mt-2">The name becomes your project workspace — and your Slack channel if a floor is connected.</p>`}
|
|
406
426
|
</div>
|
|
407
427
|
</div>`);
|
|
408
428
|
}
|
|
@@ -474,7 +494,8 @@ function autoRefreshScript(token: string): string {
|
|
|
474
494
|
// Same three sections, but each shows its setup prompt instead of live data.
|
|
475
495
|
|
|
476
496
|
export function renderSetupView(ctx: ViewContext, devices: DeviceFixture[] = []): string {
|
|
477
|
-
const
|
|
497
|
+
const floorDone = ctx.slackConnected || ctx.floorSkipped;
|
|
498
|
+
const nextStep = !floorDone ? "floor" : !ctx.cliInstalled ? "devices" : "sessions";
|
|
478
499
|
const stepState = (step: string): "done" | "active" | "future" => {
|
|
479
500
|
const order = ["floor", "devices", "sessions"];
|
|
480
501
|
const nextIdx = order.indexOf(nextStep);
|